Go Struct超詳細講解

張君鴻發表於2019-04-07

Go中提供了對struct的支援,struct,中文翻譯稱為結構體,與陣列一樣,屬於複合型別,並非引用型別。

Go語言的struct,與C語言中的struct或其他語言的class類似,但也有很不同的地方,需要深入學習,方能區別。

注意複合型別與引用型別之間的區別,這應該也是值傳遞和引用傳遞的區別吧。

定義

使用struct關鍵字可以定義一個結構體,結構體中的成員,稱為結構體的欄位。

type Member struct {
    id          int
    name, email string
    gender, age int
}
複製程式碼

上面的程式碼中,我們定義了一個包含5個欄位的結構體,可以看到,相同型別nameemailgenderage在同一行中定義,但比較好的程式設計習慣是每一行只定義一個欄位,如:

type Member struct {
    id     int
    name   string
    email  string
    gender int
    age    int
}
複製程式碼

當然,結構體也可以不包含任何欄位,稱為空結構體,struct{}表示一個空的結構體,注意,直接定義一個空的結構體並沒有意義,但在併發程式設計中,channel之間的通訊,可以使用一個struct{}作為訊號量。

ch := make(chan struct{})
ch <- struct{}{}
複製程式碼

使用

上面的例子中,我們定義了Member結構體型別,接下就可以這個自定義的型別建立變數了。

直接定義變數,這個使用方式並沒有為欄位賦初始值,因此所有欄位都會被自動賦予自已型別的零值,比如name的值為空字串"",age的值為0。

var m1 Member//所有欄位均為空值
複製程式碼

使用字面量建立變數,這種使用方式,可以在大括號中為結構體的成員賦初始值,有兩種賦初始值的方式,一種是按欄位在結構體中的順序賦值,下面程式碼中m2就是使用這種方式,這種方式要求所有的欄位都必須賦值,因此如果欄位太多,每個欄位都要賦值,會很繁瑣,另一種則使用欄位名為指定欄位賦值,如下面程式碼中變數m3的建立,使用這種方式,對於其他沒有指定的欄位,則使用該欄位型別的零值作為初始化值。

var m2 = Member{1,"小明","xiaoming@163.com",1,18} // 簡短變數宣告方式:m2 := Member{1,"小明","xiaoming@163.com",1,18}
var m3 = Member{id:2,"name":"小紅"}// 簡短變數宣告方式:m3 := Member{id:2,"name":"小紅"}
複製程式碼

訪問欄位

通過變數名,使用逗號(.),可以訪問結構體型別中的欄位,或為欄位賦值,也可以對欄位進行取址(&)操作。

fmt.Println(m2.name)//輸出:小明
m3.name = "小花"
fmt.Println(m3.name)//輸出:小花

age := &m3.age
*age = 20
fmt.Println(m3.age)//20
複製程式碼

指標結構體

結構體與陣列一樣,都是值傳遞,比如當把陣列或結構體作為實參傳給函式的形參時,會複製一個副本,所以為了提高效能,一般不會把陣列直接傳遞給函式,而是使用切片(引用型別)代替,而把結構體傳給函式時,可以使用指標結構體

指標結構體,即一個指向結構體的指標,宣告結構體變數時,在結構體型別前加*號,便宣告一個指向結構體的指標,如:

注意,指標型別為引用型別,宣告結構體指標時,如果未初始化,則初始值為nil,只有初始化後,才能訪問欄位或為欄位賦值。

var m1 *Member
m1.name = "小明"//錯誤用法,未初始化,m1為nil

m1 = &Member{}
m1.name = "小明"//初始化後,結構體指標指向某個結構體地址,才能訪問欄位,為欄位賦值。 

複製程式碼

另外,使用Go內建new()函式,可以分配記憶體來初始化結構休,並返回分配的記憶體指標,因為已經初始化了,所以可以直接訪問欄位。

var m2 = new(Member)
m2.name = "小紅"
複製程式碼

我們知道,如果將結構體轉給函式,只是複製結構體的副本,如果在函式內修改結構體欄位值,外面的結構體並不會受影響,而如果將結構體指標傳給函式,則在函式中使用指標對結構體所做的修改,都會影響到指標指向的結構體。

func main() {
    m1 := Member{}
    m2 := new(Member)
    Change(m1,m2)
    fmt.Println(m1,m2)
}

func Change(m1 Member,m2 *Member){
    m1.Name = "小明"
    m2.Name = "小紅"
}
複製程式碼

可見性

上面的例子中,我們定義結構體欄位名首字母是小寫的,這意味著這些欄位在包外不可見,因而無法在其他包中被訪問,只允許包內訪問。

下面的例子中,我們將Member宣告在member包中,而後在main包中建立一個變數,但由於結構體的欄位包外不可見,因此無法為欄位賦初始值,無法按欄位還是按索引賦值,都會引發panic錯誤。

package member
type Member struct {
    id     int
    name   string
    email  string
    gender int
    age    int
}

package main

fun main(){
    var m = member.Member{1,"小明","xiaoming@163.com",1,18}//會引發panic錯誤
}
複製程式碼

因此,如果想在一個包中訪問另一個包中結構體的欄位,則必須是大寫字母開頭的變數,即可匯出的變數,如:

type Member struct {
    Id     int
    Name   string
    Email  string
    Gender int
    Age    int
}
複製程式碼

Tags

在定義結構體欄位時,除欄位名稱和資料型別外,還可以使用反引號為結構體欄位宣告元資訊,這種元資訊稱為Tag,用於編譯階段關聯到欄位當中,如我們將上面例子中的結構體修改為:

type Member struct {
    Id     int    `json:"id,-"`
    Name   string `json:"name"`
    Email  string `json:"email"`
    Gender int    `json:"gender,"`
    Age    int    `json:"age"`
}
複製程式碼

上面例子演示的是使用encoding/json包編碼或解碼結構體時使用的Tag資訊。

Tag由反引號括起來的一系列用空格分隔的key:"value"鍵值對組成,如:

Id int `json:"id" gorm:"AUTO_INCREMENT"`
複製程式碼

特性

下面總結幾點結構體的相關特性:

值傳遞

結構體與陣列一樣,是複合型別,無論是作為實參傳遞給函式時,還是賦值給其他變數,都是值傳遞,即復一個副本。

沒有繼承

Go語言是支援物件導向程式設計的,但卻沒有繼承的概念,在結構體中,可以通過組合其他結構體來構建更復雜的結構體。

結構體不能包含自己

一個結構體,並沒有包含自身,比如Member中的欄位不能是Member型別,但卻可能是*Member。

方法

在Go語言中,將函式繫結到具體的型別中,則稱該函式是該型別的方法,其定義的方式是在func與函式名稱之間加上具體型別變數,這個型別變數稱為方法接收器,如:

注意,並不是只有結構體才能繫結方法,任何型別都可以繫結方法,只是我們這裡介紹將方法繫結到結構體中。

func setName(m Member,name string){//普通函式
    m.Name = name
}

func (m Member)setName(name string){//繫結到Member結構體的方法
    m.Name = name
}
複製程式碼

從上面的例子中,我們可以看出,通過方法接收器可以訪問結構體的欄位,這類似其他程式語言中的this關鍵詞,但在Go語言中,只是一個變數名而已,我們可以任意命名方法接收器

呼叫結構體的方法,與呼叫欄位一樣:

m := Member{}
m.setName("小明")
fmt.Println(m.Name)//輸出為空
複製程式碼

上面的程式碼中,我們會很奇怪,不是呼叫setName()方法設定了欄位Name的值了嗎?為什麼還是輸出為空呢?

這是因為,結構體是值傳遞,當我們呼叫setName時,方法接收器接收到是隻是結構體變數的一個副本,通過副本對值進行修復,並不會影響呼叫者,因此,我們可以將方法接收器定義為指標變數,就可達到修改結構體的目的了。

func (m *Member)setName(name string){/將Member改為*Member
    m.Name = name
}

m := Member{}
m.setName("小明")
fmt.Println(m.Name)//小明
複製程式碼

方法和欄位一樣,如果首字母為小寫,則只允許在包內可見,在其他包中是無法訪問的,因此,如果要在其他包中訪問setName,則應該將方法名改為SetName

組合

我們知道,結構體中並沒有繼承的概念,其實,在Go語言中也沒有繼承的概念,Go語言的程式設計哲學裡,推薦使用組合的方式來達到程式碼複用效果。

什麼是組合

組合,可以理解為定義一個結構體中,其欄位可以是其他的結構體,這樣,不同的結構體就可以共用相同的欄位。

注意,在記得我們前面提過的,結構體不能包含自身,但可能包含指向自身的結構體指標。

例如,我們定義了一個名為Animal表示動物,如果我們想定義一個結構體表示貓,如:

type Animal struct {
    Name   string  //名稱
    Color  string  //顏色
    Height float32 //身高
    Weight float32 //體重
    Age    int     //年齡
}
//奔跑
func (a Animal)Run() {
    fmt.Println(a.Name + "is running")
}
//吃東西
func (a Animal)Eat() {
    fmt.Println(a.Name + "is eating")
}

type Cat struct {
    a Animal
}

func main() {
    var c = Cat{
	    a: Animal{
            Name:   "貓貓",
            Color:  "橙色",
            Weight: 10,
            Height: 30,
            Age:    5,
        },
    }
    fmt.Println(c.a.Name)
    c.a.Run()
}

複製程式碼

可以看到,我們定義Cat結構體時,可以把Animal結構體作為Cat的欄位。

匿名欄位

上面的例子,我們看到,把Animal結構體作為Cat的欄位時,其變數名為a,所以我們訪問Animal的方法時,語法為c.a.Run(),這種通過葉子屬性訪問某個欄位型別所帶的方法和欄位用法非常繁瑣。

Go語言支援直接將型別作為結構體的欄位,而不需要取變數名,這種欄位叫匿名欄位,如:

type Lion struct {
	Animal //匿名欄位
}

func main(){
    var lion = Lion{
        Animal{
            Name:  "小獅子",
            Color: "灰色",
        },
    }
    lion.Run()
    fmt.Println(lion.Name)
}
複製程式碼

通過上面例子,可以看到,通過匿名欄位組合其他型別,而後訪問匿名欄位型別所帶的方法和欄位時,不需要使用葉子屬性,非常方便。

小結

在Go語言程式設計中,結構體大概算是使用得最多的資料型別了,通過定義不同欄位和方法的結構體,抽象組合不同的結構體,這大概便是Go語言中對物件導向程式設計了。

相關文章