1. 程式人生 > >go學習筆記(2):資料結構

go學習筆記(2):資料結構

Go語言不是一門面向物件的語言,沒有物件和繼承,也沒有面向物件的多型、重寫相關特性。

Go所擁有的是資料結構,它可以關聯方法。Go也支援簡單但高效的組合(Composition),請搜尋面向物件和組合。

雖然Go不支援面向物件,但Go通過定義資料結構的方式,也能實現與Class相似的功能。

一個簡單的例子,定義一個Animal資料結構:

type Animal struct {
    name string
    speak string
}

這就像是定義了一個class,有自己的屬性。

在稍後,將會介紹如何向這個資料結構中新增方法,就像為類定義方法一樣。不過現在,先簡單介紹下資料結構。

資料結構的定義和初始化

除了int、string等內建的資料型別,我們可以定義structure來自定義資料型別。

建立資料結構最簡單的方式:

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都指向這個資料結構,有什麼區別?我打算用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。

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()
}