go基礎系列:結構struct


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都指向這個數據結構,有什么區別?

實際上,賦值給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()
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM