Go基礎系列:struct和巢狀struct
struct
struct定義結構,結構由欄位(field)組成,每個field都有所屬資料型別,在一個struct中,每個欄位名都必須唯一。
說白了就是拿來儲存資料的,只不過可自定義化的程度很高,用法很靈活,Go中不少功能依賴於結構,就這樣一個角色。
Go中不支援面向物件,面向物件中描述事物的類的重擔由struct來挑,這種模式稱為組合(composite)。面向物件中父類與子類、類與物件的關係是is a
的關係,例如Horse is a Animal
,Go中的組合則是外部struct與內部struct的關係、struct例項與struct的關係,它們是has a
的關係。Go中通過struct的composite,可以"模仿"很多面向物件中的行為,它們很"像"。
定義struct
定義struct的格式如下:
type identifier struct {
field1 type1
field2 type2
…
}
// 或者
type T struct { a, b int }
理論上,每個欄位都是有具有唯一性的名字的,但如果確定某個欄位不會被使用,可以將其名稱定義為空識別符號_
來丟棄掉:
type T struct {
_ string
a int
}
每個欄位都有型別,可以是任意型別,包括內建簡單資料型別、其它自定義的struct型別、當前struct型別本身、介面、函式、channel等等。
如果某幾個欄位型別相同,可以縮寫在同一行:
type mytype struct {
a,b int
c string
}
構造struct例項
定義了struct,就表示定義了一個數據結構,或者說資料型別,也或者說定義了一個類。總而言之,定義了struct,就具備了成員屬性,就可以作為一個抽象的模板,可以根據這個抽象模板生成具體的例項,也就是所謂的"物件"。
例如:
type person struct{
name string
age int
}
// 初始化一個person例項
var p person
這裡的p就是一個具體的person例項,它根據抽象的模板person構造而出,具有具體的屬性name和age的值,雖然初始化時它的各個欄位都是0值。換句話說,p是一個具體的人。
struct初始化時,會做預設的賦0初始化,會給它的每個欄位根據它們的資料型別賦予對應的0值。例如int型別是數值0,string型別是"",引用型別是nil等。
因為p已經是初始化person之後的例項了,它已經具備了實實在在存在的屬性(即欄位),所以可以直接訪問它的各個屬性。這裡通過訪問屬性的方式p.FIELD
為各個欄位進行賦值。
// 為person例項的屬性賦值,定義具體的person
p.name = "longshuai"
p.age = 23
獲取某個屬性的值:
fmt.Println(p.name) // 輸出"longshuai"
也可以直接賦值定義struct的屬性來生成struct的例項,它會根據值推斷出p的型別。
var p = person{name:"longshuai",age:23}
p := person{name:"longshuai",age:23}
// 不給定名稱賦值,必須按欄位順序
p := person{"longshuai",23}
p := person{age:23}
p.name = "longshuai"
如果struct的屬性分行賦值,則必須不能省略每個欄位後面的逗號",",否則就會報錯。這為未來移除、新增屬性都帶來方便:
p := person{
name:"longshuai",
age:23, // 這個逗號不能省略
}
除此之外,還可以使用new()函式或&TYPE{}
的方式來構造struct例項,它會為struct分配記憶體,為各個欄位做好預設的賦0初始化。它們是等價的,都返回資料物件的指標給變數,實際上&TYPE{}
的底層會呼叫new()。
p := new(person)
p := &person{}
// 生成物件後,為屬性賦值
p.name = "longshuai"
p.age = 23
使用&TYPE{}
的方式也可以初始化賦值,但new()不行:
p := &person{
name:"longshuai",
age:23,
}
選擇new()還是選擇&TYPE{}
的方式構造例項?完全隨意,它們是等價的。但如果想要初始化時就賦值,可以考慮使用&TYPE{}
的方式。
struct的值和指標
下面三種方式都可以構造person struct的例項p:
p1 := person{}
p2 := &person{}
p3 := new(person)
但p1和p2、p3是不一樣的,輸出一下就知道了:
package main
import (
"fmt"
)
type person struct {
name string
age int
}
func main() {
p1 := person{}
p2 := &person{}
p3 := new(person)
fmt.Println(p1)
fmt.Println(p2)
fmt.Println(p3)
}
結果:
{ 0}
&{ 0}
&{ 0}
p1、p2、p3都是person struct的例項,但p2和p3是完全等價的,它們都指向例項的指標,指標中儲存的是例項的地址,所以指標再指向例項,p1則是直接指向例項。這三個變數與person struct例項的指向關係如下:
變數名 指標 資料物件(例項)
-------------------------------
p1(addr) -------------> { 0}
p2 -----> ptr(addr) --> { 0}
p3 -----> ptr(addr) --> { 0}
所以p1和ptr(addr)儲存的都是資料物件的地址,p2和p3則儲存ptr(addr)的地址。通常,將指向指標的變數(p1、p2)直接稱為指標,將直接指向資料物件的變數(p1)稱為物件本身,因為指向資料物件的內容就是資料物件的地址,其中ptr(addr)和p1儲存的都是例項物件的地址。
但儘管一個是資料物件值,一個是指標,它們都是資料物件的例項。也就是說,p1.name
和p2.name
都能訪問對應例項的屬性。
那var p4 *person
呢,它是什麼?該語句表示p4是一個指標,它的指向物件是person型別的,但因為它是一個指標,它將初始化為nil,即表示沒有指向目標。但已經明確表示了,p4所指向的是一個儲存資料物件地址的指標。也就是說,目前為止,p4的指向關係如下:
p4 -> ptr(nil)
既然p4是一個指標,那麼可以將&person{}
或new(person)
賦值給p4。
var p4 *person
p4 = &person{
name:"longshuai",
age:23,
}
fmt.Println(p4)
上面的程式碼將輸出:
&{longshuai 23}
傳值 or 傳指標
Go函式給引數傳遞值的時候是以複製的方式進行的。
複製傳值時,如果函式的引數是一個struct物件,將直接複製整個資料結構的副本傳遞給函式,這有兩個問題:
- 函式內部無法修改傳遞給函式的原始資料結構,它修改的只是原始資料結構拷貝後的副本
- 如果傳遞的原始資料結構很大,完整地複製出一個副本開銷並不小
所以,如果條件允許,應當給需要struct例項作為引數的函式傳struct的指標。例如:
func add(p *person){...}
既然要傳指標,那struct的指標何來?自然是通過&
符號來獲取。分兩種情況,建立成功和尚未建立的例項。
對於已經建立成功的struct例項p
,如果這個例項是一個值而非指標(即p->{person_fields}
),那麼可以&p
來獲取這個已存在的例項的指標,然後傳遞給函式,如add(&p)
。
對於尚未建立的struct例項,可以使用&person{}
或者new(person)
的方式直接生成例項的指標p,雖然是指標,但Go能自動解析成例項物件。然後將這個指標p傳遞給函式即可。如:
p1 := new(person)
p2 := &person{}
add(p1)
add(p2)
struct field的tag屬性
在struct中,field除了名稱和資料型別,還可以有一個tag屬性。tag屬性用於"註釋"各個欄位,除了reflect包,正常的程式中都無法使用這個tag屬性。
type TagType struct { // tags
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}
匿名欄位和struct巢狀
struct中的欄位可以不用給名稱,這時稱為匿名欄位。匿名欄位的名稱強制和型別相同。例如:
type animal struct {
name string
age int
}
type Horse struct{
int
animal
sound string
}
上面的Horse中有兩個匿名欄位int
和animal
,它的名稱和型別都是int和animal。等價於:
type Horse struct{
int int
animal animal
sound string
}
顯然,上面Horse中嵌套了其它的struct(如animal)。其中animal稱為內部struct,Horse稱為外部struct。
以下是一個巢狀struct的簡單示例:
package main
import (
"fmt"
)
type inner struct {
in1 int
in2 int
}
type outer struct {
ou1 int
ou2 int
int
inner
}
func main() {
o := new(outer)
o.ou1 = 1
o.ou2 = 2
o.int = 3
o.in1 = 4
o.in2 = 5
fmt.Println(o.ou1) // 1
fmt.Println(o.ou2) // 2
fmt.Println(o.int) // 3
fmt.Println(o.in1) // 4
fmt.Println(o.in2) // 5
}
上面的o
是outer struct的例項,但o
除了具有自己的顯式欄位ou1和ou2,還具備int欄位和inner欄位,它們都是巢狀欄位。一被巢狀,內部struct的屬性也將被外部struct獲取,所以o.int
、o.in1
、o.in2
都屬於o
。也就是說,外部struct has a 內部struct
,或者稱為struct has a field
。
上面的outer例項,也可以直接賦值構建:
o := outer{1,2,3,inner{4,5}}
在賦值inner中的in1和in2時不能少了inner{}
,否則會認為in1、in2是直接屬於outer,而非巢狀屬於outer。
顯然,struct的巢狀類似於面向物件的繼承。只不過繼承的關係模式是"子類 is a 父類",例如"轎車是一種汽車",而巢狀struct的關係模式是外部struct has a 內部struct
,正如上面示例中outer擁有inner
。
巢狀struct的名稱衝突問題
假如外部struct中的欄位名和內部struct的欄位名相同,會如何?
有以下兩個名稱衝突的規則:
- 外部struct覆蓋內部struct的同名欄位、同名方法
- 同級別的struct出現同名欄位、方法將報錯
第一個規則使得Go struct能夠實現面向物件中的重寫(override),而且可以重寫欄位、重寫方法。
第二個規則使得同名屬性不會出現歧義。例如:
type A struct {
a int
b int
}
type B struct {
b float32
c string
d string
}
type C struct {
A
B
a string
c string
}
var c C
按照規則(1),直屬於C的a和c會分別覆蓋A.a和B.c。可以直接使用c.a、c.c分別訪問直屬於C中的a、c欄位,使用c.d或c.B.d都訪問屬於巢狀的B.d欄位。如果想要訪問內部struct中被覆蓋的屬性,可以c.A.a的方式訪問。
按照規則(2),A和B在C中是同級別的巢狀結構,所以A.b和B.b是衝突的,將會報錯,因為當呼叫c.b的時候不知道呼叫的是c.A.b還是c.B.b。