go基礎系列:結構struct

Go語言不是一門面向對象的語言,沒有對象和繼承,也沒有面向對象的多態、重寫相關特性。數據結構

Go所擁有的是數據結構,它能夠關聯方法。Go也支持簡單但高效的組合(Composition),請搜索面向對象和組合。函數

雖然Go不支持面向對象,但Go經過定義數據結構的方式,也能實現與Class類似的功能。spa

一個簡單的例子,定義一個Animal數據結構:3d

type Animal struct {
    name string
    speak string
}

這就像是定義了一個class,有本身的屬性。指針

在稍後,將會介紹如何向這個數據結構中添加方法,就像爲類定義方法同樣。不過如今,先簡單介紹下數據結構。code

數據結構的定義和初始化

除了int、string等內置的數據類型,咱們能夠定義structure來自定義數據類型。對象

建立數據結構最簡單的方式:blog

bm_horse := Animal{
    name:"baima",
    speak:"neigh",
}

注意,上面最後一個逗號","不能省略,Go會報錯,這個逗號有助於咱們去擴展這個結構,因此習慣後,這是一個很好的特性。繼承

上面bm_horse := Animal{}中,Animal就像是一個類,這個聲明和賦值的操做就像建立了一個Animal類的實例,也就是對象,其中對象名爲bm_horse,它是這個實例的惟一標識符。這個對象具備屬性name和speak,它們是每一個對象所擁有的key,且它們都有本身的值。從面向對象的角度上考慮,這其實很容易理解。內存

還能夠根據Animal數據結構再建立另一個實例:

hm_horse := Animal{
    name:"heima",
    speak:"neigh",
}

bm_horsehm_horse都是Animal的實例,根據Animal數據結構建立而來,這兩個實例都擁有本身的數據結構。以下圖:

從另外一種角度上看,bm_horse這個名稱實際上是這個數據結構的一個引用。再進一步考慮,其實面向對象的類和對象也是一種數據結構,每個對象的名稱(即bm_horse)都是對這種數據結構的引用。關於這一點,在後面介紹指針的時候將很是有助於理解。

如下是兩外兩種有效的數據結構定義方式:

// 定義空數據結構
bm_horse := Animal{}

// 或者,先定義一部分,再賦值
bm_horse := Animal {name:"baima"}
bm_horse.speak = "neigh"

此外,還能夠省略數據結構中的key部分(也就是屬性的名稱)直接爲數據結構中的屬性賦值,只不過這時賦的值必須和key的順序對應。

bm_horse := Animal{"baima","neigh"}

在數據結構的屬性數量較少的時候,這種賦值方式也是不錯的,但屬性數量多了,不建議如此賦值,由於很容易混亂。

訪問數據結構的屬性

要訪問一個數據結構中的屬性,以下:

package main

import ("fmt")

func main(){
    
    type Animal struct {
        name string
        speak string
    }

    bm_horse := Animal{"baima","neigh"}
    fmt.Println("name:",bm_horse.name)
    fmt.Println("speak:",bm_horse.speak)
}

前面說過,Animal是一個數據結構的模板(就像類同樣),不是實例,bm_horse纔是具體的實例,有本身的數據結構,因此,要訪問本身數據結構中的數據,能夠經過本身的名稱來訪問本身的屬性:

bm_horse.name
bm_horse.speak

指針

bm_horse := Animal{}表示返回一個數據結構給bm_horse,bm_horse指向這個數據結構,也能夠說bm_horse是這個數據結構的引用。

除此,還有另外一種賦值方式,比較下兩種賦值方式:

bm_horse := Animal{"baima","neigh"}
ref_bm_horse := &Animal{"baima","neigh"}

這兩種賦值方式,有何不一樣?

:=操做符都聲明左邊的變量,並賦值變量。賦值的內容基本神似:

  • 第一種將整個數據結構賦值給變量bm_horsebm_horse今後變成Animal的實例;
  • 第二種使用了一個特殊符號&在數據結構前面,它表示返回這個數據結構的引用,也就是這個數據結構的地址,因此ref_bm_horse也指向這個數據結構。

bm_horseref_bm_horse都指向這個數據結構,有什麼區別?

實際上,賦值給bm_horse的是Animal實例的地址,賦值給ref_bm_horse是一箇中間的指針,這個指針裏保存了Animal實例的地址。它們的關係至關於:

bm_horse -> Animal{}
ref_bm_horse -> Pointer -> Animal{}

其中Pointer在內存中佔用一個長度爲一個機器字長的單獨數據塊,64位機器上一個機器字長是8字節,因此賦值給ref_bm_horse的這個8字節長度的指針地址,這個指針地址再指向Animal{},而bm_horse則是直接指向Animal{}

若是還不明白,我打算用perl語言的語法來解釋它們的區別,由於C和Go的指針太過"晦澀"。

perl中的引用

在Perl中,一個hash結構使用%符號來表示,例如:

%Animal = (
    name => "baima",
    speak => "neigh",
);

這裏的"Animal"表示的是這個hash結構的名稱,而後經過%+NAME的方式來引用這個hash數據結構。其實hash結構的名稱"Animal"就是這個hash結構的一個引用,表示指向這個hash結構,只不過這個Animal是建立hash結構是就指定好的已命名的引用。

perl中還支持顯式地建立一個引用。例如:

$ref_myhash = \%Animal;

%Animal表示的是hash數據結構,加上\表示這個數據結構的一個引用,這個引用指向這個hash數據結構。perl中的引用是一個變量,因此使用$ref_myhash表示。

也就是說,hash結構的名稱Animal$ref_myhash是徹底等價的,都是hash結構的引用,也就是指向這個數據結構,也就是指針。因此,%Animal能表示取hash結構的屬性,%$ref_myhash也能表示取hash結構的屬性,這種從引用取回hash數據結構的方式稱爲"解除引用"。

另外,$ref_myhash是一個變量類型,而%Animal是一個hash類型。

引用變量能夠賦值給另外一個引用變量,這樣兩個引用都將指向同一個數據結構:

$ref_myhash1 = $ref_myhash;

如今,$ref_myhash$ref_myhash1Animal都指向同一個數據結構。

Go中的指針:引用

總結下上面perl相關的代碼:

%Animal = (
    name => "baima",
    speak => "neigh",
);

$ref_myhash = \%Animal;
$ref_myhash1 = $ref_myhash;

%Animal是hash結構,Animal$ref_myhash$ref_myhash1都是這個hash結構的引用。

回到Go語言的數據結構:

bm_horse :=  Animal{}
hm_horse := &Animal{}

這裏的Animal{}是一個數據結構,至關於perl中的hash數據結構:

(
    name => "baima",
    speak => "neigh",
)

bm_horse是數據結構的直接賦值對象,它直接表示數據結構,因此它等價於前面perl中的%Animal。而hm_horseAnimal{}數據結構的引用,它等價於perl中的Animal$ref_myhash$ref_myhash1

之因此Go中的指針很差理解,就是由於數據結構bm_horse和引用hm_horse都沒有任何額外的標註,看上去都像是一種變量。但其實它們是兩種不一樣的數據類型:一種是數據結構,一種是引用。

Go中的星號"*"

星號有兩種用法:

  • x *int表示變量x是一個引用,這個引用指向的目標數據是int類型。更通用的形式是x *TYPE
  • *x表示x是一個引用,*x表示解除這個引用,取回x所指向的數據結構,也就是說這是 一個數據結構,只不過這個數據結構多是內置數據類型,也多是自定義的數據結構

x *int的x是一個指向int類型的引用,而&y返回的也是一個引用,因此&y的y若是是int類型的數據,&y能夠賦值給x *int的x。

注意,x的數據類型是*int,不是int,雖然x所指向的是數據類型是int。就像前面perl中的引用只是一個變量,而其指向的倒是一個hash數據結構同樣。

*x表明的是數據結構自身,因此若是爲其賦值(如*x = 2),則新賦的值將直接保存到x指向的數據中。

例如:

package main

import ("fmt")

func main(){
    var a *int
    c := 2
    a = &c
    d := *a
    fmt.Println(*a)   // 輸出2
    fmt.Println(d)    // 輸出2
}

var a *int定義了一個指向int類型的數據結構的引用。a = &c中,由於&c返回的是一個引用,指向的是數據結構c,c是int類型的數據結構,將其賦值給a,因此a也指向c這個數據結構,也就是說*a的值將等於2。因此d := *a賦值後,d自身是一個int類型的數據結構,其值爲2。

package main

import "fmt"

func main() {
	var i int = 10
	println("i addr: ", &i)  // 數據對象10的地址:0xc042064058

	var ptr *int = &i
	fmt.Printf("ptr=%v\n", ptr)        // 0xc042064058
	fmt.Printf("ptr addr: %v\n", &ptr) // 指針對象ptr的地址:0xc042084018
	fmt.Printf("ptr地址: %v\n", *&ptr) // 指針對象ptr的值0xc042064058
	fmt.Printf("ptr->value: %v", *ptr) // 10
}

Go函數參數傳值

Go函數給參數傳遞值的時候是以複製的方式進行的

由於複製傳值的方式,若是函數的參數是一個數據結構,將直接複製整個數據結構的副本傳遞給函數,這有兩個問題:

  1. 函數內部沒法修改傳遞給函數的原始數據結構,它修改的只是原始數據結構拷貝後的副本
  2. 若是傳遞的原始數據結構很大,完整地複製出一個副本開銷並不小

例如,第一個問題:

package main

import ("fmt")

type Animal struct {
	name string
	weight int
}

func main(){
    bm_horse := Animal{
        name: "baima",
        weight: 60,
    }
    add(bm_horse)
    fmt.Println(bm_horse.weight)
}

func add(a Animal){
    a.weight += 10
}

上面的輸出結果仍然爲60。add函數用於修改Animal的實例數據結構中的weight屬性。當執行add(bm_horse)的時候,bm_horse傳遞給add()函數,但並非直接傳遞給add()函數,而是複製一份bm_horse的副本賦值給add函數的參數a,因此add()中修改的a.weight的屬性是bm_horse的副本,而不是直接修改的bm_horse,因此上面的輸出結果仍然爲60。

爲了修改bm_horse所在的數據結構的值,須要使用引用(指針)的方式傳值。

只需修改兩個地方便可:

package main

import ("fmt")

type Animal struct {
	name string
	weight int
}

func main(){
    bm_horse := &Animal{
        name: "baima",
        weight: 60,
    }
    add(bm_horse)
    fmt.Println(bm_horse.weight)
}

func add(a *Animal){
    a.weight += 10
}

爲了修改傳遞給函數參數的數據結構,這個參數必須是直接指向這個數據結構的。因此使用add(a *Animal),既然a是一個Animal數據結構的一個實例的引用,因此調用add()的時候,傳遞給add()中的參數必須是一個Animal數據結構的引用,因此bm_horse的定義語句中使用&符號。

當調用到add(bm_horse)的時候,由於bm_horse是一個引用,因此賦值給函數參數a時,複製的是這個數據結構的引用,使得add能直接修改其外部的數據結構屬性。

大多數時候,傳遞給函數的數據結構都是它們的引用,但極少數時候也有需求直接傳遞數據結構。

方法:屬於數據結構的函數

能夠爲數據結構定義屬於本身的函數。

package main
import ("fmt")

type Animal struct {
    name string
    weight int
}

func (a *Animal) add() {
    a.weight += 10
}

func main() {
    bm_horse := &Animal{"baima",70}
    bm_horse.add()
    fmt.Println(bm_horse.weight)    // 輸出80
}

上面的add()函數定義方式func (a *Animal) add(){},它所表示的就是定義於數據結構Animal上的函數,就像類的實例方法同樣,只要是屬於這個數據結構的實例,都能直接調用這個函數,正如bm_horse.add()同樣。

構造器

面向對象中有構造器(也稱爲構造方法),能夠根據類構造出類的實例:對象。

Go雖然不支持面向對象,沒有構造器的概念,但也具備構造器的功能,畢竟構造器只是一個方法而已。只要一個函數可以根據數據結構返回這個數據結構的一個實例對象,就能夠稱之爲"構造器"。

例如,如下是Animal數據結構的一個構造函數:

func newAnimal(n string,w int) *Animal {
    return &Animal{
        name: n,
        weight: w,
    }
}

如下返回的是非引用類型的數據結構:

func newAnimal(n string,w int) Animal {
    return Animal{
        name: n,
        weigth: w,
    }
}

通常上面的方法類型稱爲工廠方法,就像工廠同樣根據模板不斷生成產品。但對於建立數據結構的實例來講,通常仍是會採用內置的new()方式。

new函數

儘管Go沒有構造器,但Go還有一個內置的new()函數用於爲一個數據結構分配內存。其中new(x)等價於&x{},如下兩語句等價:

bm_horse := new(Animal)
bm_horse := &Animal{}

使用哪一種方式取決於本身。但若是要進行初始化賦值,通常採用第二種方法,可讀性更強:

# 第一種方式
bm_horse := new(Animal)
bm_horse.name = "baima"
bm_horse.weight = 60

# 第二種方式
bm_horse := &Animal{
    name: "baima",
    weight: 60,
}

擴展數據結構的字段

在前面出現的數據結構中的字段數據類型都是簡簡單單的內置類型:string、int。但數據結構中的字段能夠更復雜,例如能夠是map、array等,還能夠是自定義的數據類型(數據結構)。

例如,將一個指向同類型數據結構的字段添加到數據結構中:

type Animal struct {
    name   string
    weight int
    father *Animal
}

其中在此處的*Animal所表示的數據結構實例極可能是其它的Animal實例對象。

上面定義了father,還能夠定義son,sister等等。

例如:

bm_horse := &Animal{
    name: "baima",
    weight: 60,
    father: &Animal{
        name: "hongma",
        weight: 80,
        father: nil,
    },
}

composition

Go語言支持Composition(組合),它表示的是在一個數據結構中嵌套另外一個數據結構的行爲。

package main

import (
    "fmt"
)

type Animal struct {
    name   string
    weight int
}

type Horse struct {
    *Animal                  // 注意此行
    speak string
}

func (a *Animal) hello() {
    fmt.Println(a.name)
    fmt.Println(a.weight)
    //fmt.Println(a.speak)
}

func main() {
    bm_horse := &Horse{
        Animal: &Animal{        // 注意此行
            name:   "baima",
            weight: 60,
        },
        speak: "neigh",
    }
    bm_horse.hello()
}

上面的Horse數據結構中包含了一行*Animal,表示Animal的數據結構插入到Horse的結構中,這就像是一種面向對象的類繼承。注意,沒有給該字段顯式命名,但能夠隱式地訪問Horse組合結構中的字段和函數。

另外,在構建Horse實例的時候,必須顯式爲其指定字段名(儘管數據結構中並無指定其名稱),且字段的名稱必須和數據結構的名稱徹底相同。

而後調用屬於Animal數據結構的hello方法,它只能訪問Animal中的屬性,因此沒法訪問speak屬性。

不少人認爲這種代碼共享的方式比面向對象的繼承更加健壯。

Go中的重載overload

例如,將上面屬於Animal數據結構的hello函數重載爲屬於Horse數據結構的hello函數:

package main

import (
    "fmt"
)

type Animal struct {
    name   string
    weight int
}

type Horse struct {
    *Animal                  // 注意此行
    speak string
}

func (h *Horse) hello() {
    fmt.Println(h.name)
    fmt.Println(h.weight)
    fmt.Println(h.speak)
}

func main() {
    bm_horse := &Horse{
        Animal: &Animal{       // 注意此行
            name:   "baima",
            weight: 60,
        },
        speak: "neigh",
    }
    bm_horse.hello()
}
相關文章
相關標籤/搜索