Go基礎系列:struct和嵌套struct

原文作者:駿馬金龍 來源:博客園

struct

struct定義結構,結構由字段(field)組成,每個field都有所屬數據類型,在一個struct中,每個字段名都必須唯一。

說白了就是拿來存儲數據的,只不過可自定義化的程度很高,用法很靈活,Go中不少功能依賴於結構,就這樣一個角色。

Go中不支持面向對象,面向對象中描述事物的類的重擔由struct來挑。比如面向對象中的繼承,可以使用組合(composite)來實現:struct中嵌套一個(或多個)類型。面向對象中父類與子類、類與對象的關係是is a的關係,例如Horse is a Animal,Go中的組合則是外部struct與內部struct的關係、struct實例與struct的關係,它們是has a的關係。Go中通過struct的composite,可以"模仿"很多面向對象中的行爲,它們很"像"。

定義struct

定義struct的格式如下:

1type identifier struct {
2    field1 type1
3    field2 type2
4    …
5}// 或者type T struct { a, b int }

理論上,每個字段都是有具有唯一性的名字的,但如果確定某個字段不會被使用,可以將其名稱定義爲空標識符_來丟棄掉:

1type T struct {
2    _ string
3    a int}

每個字段都有類型,可以是任意類型,包括內置簡單數據類型、其它自定義的struct類型、當前struct類型本身、接口、函數、channel等等。

如果某幾個字段類型相同,可以縮寫在同一行:

1type mytype struct {
2    a,b int
3    c string}

構造struct實例

定義了struct,就表示定義了一個數據結構,或者說數據類型,也或者說定義了一個類。總而言之,定義了struct,就具備了成員屬性,就可以作爲一個抽象的模板,可以根據這個抽象模板生成具體的實例,也就是所謂的"對象"。

例如:

1type person struct{
2    name string
3    age int}// 初始化一個person實例var p person

這裏的p就是一個具體的person實例,它根據抽象的模板person構造而出,具有具體的屬性name和age的值,雖然初始化時它的各個字段都是0值。換句話說,p是一個具體的人。

struct初始化時,會做默認的賦0初始化,會給它的每個字段根據它們的數據類型賦予對應的0值。例如int類型是數值0,string類型是"",引用類型是nil等。

因爲p已經是初始化person之後的實例了,它已經具備了實實在在存在的屬性(即字段),所以可以直接訪問它的各個屬性。這裏通過訪問屬性的方式p.FIELD爲各個字段進行賦值。

1// 爲person實例的屬性賦值,定義具體的personp.name = "longshuai"p.age = 23

獲取某個屬性的值:

1fmt.Println(p.name) // 輸出"longshuai"

也可以直接賦值定義struct的屬性來生成struct的實例,它會根據值推斷出p的類型。

1var p = person{name:"longshuai",age:23}p := person{name:"longshuai",age:23}// 不給定名稱賦值,必須按字段順序p := person{"longshuai",23}p := person{age:23}
2p.name = "longshuai"

如果struct的屬性分行賦值,則必須不能省略每個字段後面的逗號",",否則就會報錯。這爲未來移除、添加屬性都帶來方便:

1p := person{
2    name:"longshuai",
3    age:23,     // 這個逗號不能省略}

除此之外,還可以使用new()函數或&TYPE{}的方式來構造struct實例,它會爲struct分配內存,爲各個字段做好默認的賦0初始化。它們是等價的,都返回數據對象的指針給變量,實際上&TYPE{}的底層會調用new()。

1p := new(person)
2p := &person{}// 生成對象後,爲屬性賦值p.name = "longshuai"p.age = 23

使用&TYPE{}的方式也可以初始化賦值,但new()不行:

1p := &person{
2    name:"longshuai",
3    age:23,
4}

選擇new()還是選擇&TYPE{}的方式構造實例?完全隨意,它們是等價的。但如果想要初始化時就賦值,可以考慮使用&TYPE{}的方式。

struct的值和指針

下面三種方式都可以構造person struct的實例p:

1p1 := person{}
2p2 := &person{}
3p3 := new(person)

但p1和p2、p3是不一樣的,輸出一下就知道了:

 1package mainimport (    "fmt")type person struct {
 2    name string
 3    age  int}func main() {
 4    p1 := person{}
 5    p2 := &person{}
 6    p3 := new(person)
 7    fmt.Println(p1)
 8    fmt.Println(p2)
 9    fmt.Println(p3)
10}

結果:

1{ 0}
2&{ 0}
3&{ 0}

p1、p2、p3都是person struct的實例,但p2和p3是完全等價的,它們都指向實例的指針,指針中保存的是實例的地址,所以指針再指向實例,p1則是直接指向實例。這三個變量與person struct實例的指向關係如下:

1 變量名      指針     數據對象(實例)
2-------------------------------
3p1(addr) -------------> { 0}
4p2 -----> ptr(addr) --> { 0}
5p3 -----> ptr(addr) --> { 0}

所以p1和ptr(addr)保存的都是數據對象的地址,p2和p3則保存ptr(addr)的地址。通常,將指向指針的變量(p1、p2)直接稱爲指針,將直接指向數據對象的變量(p1)稱爲對象本身,因爲指向數據對象的內容就是數據對象的地址,其中ptr(addr)和p1保存的都是實例對象的地址。

儘管一個是數據對象值,一個是指針,它們都是數據對象的實例。也就是說,p1.namep2.name都能訪問對應實例的屬性。

var p4 *person呢,它是什麼?該語句表示p4是一個指針,它的指向對象是person類型的,但因爲它是一個指針,它將初始化爲nil,即表示沒有指向目標。但已經明確表示了,p4所指向的是一個保存數據對象地址的指針。也就是說,目前爲止,p4的指向關係如下:

1p4 -> ptr(nil)

既然p4是一個指針,那麼可以將&person{}new(person)賦值給p4。

1var p4 *person
2p4 = &person{
3    name:"longshuai",
4    age:23,
5}
6fmt.Println(p4) 

上面的代碼將輸出:

1&{longshuai 23}

傳值 or 傳指針

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

複製傳值時,如果函數的參數是一個struct對象,將直接複製整個數據結構的副本傳遞給函數,這有兩個問題:

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

所以,如果條件允許,應當給需要struct實例作爲參數的函數傳struct的指針。例如:

1func add(p *person){...}

既然要傳指針,那struct的指針何來?自然是通過&符號來獲取。分兩種情況,創建成功和尚未創建的實例。

對於已經創建成功的struct實例p,如果這個實例是一個值而非指針(即p->{person_fields}),那麼可以&p來獲取這個已存在的實例的指針,然後傳遞給函數,如add(&p)

對於尚未創建的struct實例,可以使用&person{}或者new(person)的方式直接生成實例的指針p,雖然是指針,但Go能自動解析成實例對象。然後將這個指針p傳遞給函數即可。如:

1p1 := new(person)
2p2 := &person{}add(p1)add(p2)

struct field的tag屬性

在struct中,field除了名稱和數據類型,還可以有一個tag屬性。tag屬性用於"註釋"各個字段,除了reflect包,正常的程序中都無法使用這個tag屬性。

1type TagType struct { // tags
2    field1 bool   "An important answer"
3    field2 string "The name of the thing"
4    field3 int    "How much there are"}

匿名字段和struct嵌套

struct中的字段可以不用給名稱,這時稱爲匿名字段。匿名字段的名稱強制和類型相同。例如:

1type animal struct {
2    name string
3    age int}type Horse struct{    int
4    animal
5    sound string}

上面的Horse中有兩個匿名字段intanimal,它的名稱和類型都是int和animal。等價於:

1type Horse struct{    int int
2    animal animal
3    sound string}

顯然,上面Horse中嵌套了其它的struct(如animal)。其中animal稱爲內部struct,Horse稱爲外部struct。

以下是一個嵌套struct的簡單示例:

 1package mainimport (    "fmt")type inner struct {
 2    in1 int
 3    in2 int}type outer struct {
 4    ou1 int
 5    ou2 int
 6    int
 7    inner
 8}func main() {
 9    o := new(outer)
10    o.ou1 = 1
11    o.ou2 = 2
12    o.int = 3
13    o.in1 = 4
14    o.in2 = 5
15    fmt.Println(o.ou1)  // 1
16    fmt.Println(o.ou2)  // 2
17    fmt.Println(o.int)  // 3
18    fmt.Println(o.in1)  // 4
19    fmt.Println(o.in2)  // 5}

上面的o是outer struct的實例,但o除了具有自己的顯式字段ou1和ou2,還具備int字段和inner字段,它們都是嵌套字段。一被嵌套,內部struct的屬性也將被外部struct獲取,所以o.into.in1o.in2都屬於o。也就是說,外部struct has a 內部struct,或者稱爲struct has a field

輸出以下外部struct的內容就很清晰了:

1fmt.Println(o)  // 結果:&{1 2 3 {4 5}}

上面的outer實例,也可以直接賦值構建:

1o := 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。而且,從上面的示例中可以看出,Go是支持"多重繼承"的。

嵌套struct的名稱衝突問題

假如外部struct中的字段名和內部struct的字段名相同,會如何?

有以下兩個名稱衝突的規則:

  1. 外部struct覆蓋內部struct的同名字段、同名方法
  2. 同級別的struct出現同名字段、方法將報錯

第一個規則使得Go struct能夠實現面向對象中的重寫(override),而且可以重寫字段、重寫方法。

第二個規則使得同名屬性不會出現歧義。例如:

 1type A struct {
 2    a int
 3    b int}type B struct {
 4    b float32
 5    c string
 6    d string}type C struct {
 7    A
 8    B
 9    a string
10    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。

遞歸struct:嵌套自身

如果struct中嵌套的struct類型是自己的指針類型,可以用來生成特殊的數據結構:鏈表或二叉樹(雙端鏈表)。

例如,定義一個單鏈表數據結構,每個Node都指向下一個Node,最後一個Node指向空。

1type Node struct {    data string
2    ri   *Node}

以下是鏈表結構示意圖:

1 ------|----         ------|----         ------|-----
2| data | ri |  -->  | data | ri |  -->  | data | nil |
3 ------|----         ------|----         ------|----- 
4

如果給嵌套兩個自己的指針,每個結構都有一個左指針和一個右指針,分別指向它的左邊節點和右邊節點,就形成了二叉樹或雙端鏈表數據結構

二叉樹的左右節點可以留空,可隨時向其中加入某一邊加入新節點(像節點加入到樹中)。添加節點時,節點與節點之間的關係是父子關係。添加完成後,節點與節點之間的關係是父子關係或兄弟關係。

雙端鏈表有所不同,添加新節點時必須讓某節點的左節點和另一個節點的右節點關聯。例如目前已有的鏈表節點A <-> C,現在要將B節點加入到A和C的中間,即A<->B<->C,那麼A的右節點必須設置爲B,B的左節點必須設置爲A,B的右節點必須設置爲C,C的左節點必須設置爲B。也就是涉及了4次原子性操作,它們要麼全設置成功,失敗一個則鏈表被破壞。

例如,定義一個二叉樹:

1type Tree struct {    le   *Tree
2    data string
3    ri   *Tree}

最初生成二叉樹時,root節點沒有任何指向。

1// root節點:初始左右兩端爲空root := new(Tree)
2root.data = "root node"

隨着節點增加,root節點開始指向其它左節點、右節點,這些節點還可以繼續指向其它節點。向二叉樹中添加節點的時候,只需將新生成的節點賦值給它前一個節點的le或ri字段即可。例如:

1// 生成兩個新節點:初始爲空newLeft := new(Tree)
2newLeft.data = "left node"newRight := &Tree{nil, "Right node", nil}// 添加到樹中root.le = newLeft
3root.ri = newRight// 再添加一個新節點到newLeft節點的右節點anotherNode := &Tree{nil, "another Node", nil}
4newLeft.ri = anotherNode

簡單輸出這個樹中的節點:

1fmt.Println(root)fmt.Println(newLeft)fmt.Println(newRight)

輸出結果:

1&{0xc042062400 root node 0xc042062420}
2&{<nil> left node 0xc042062440}
3&{<nil> Right node <nil>}

當然,使用二叉樹的時候,必須爲二叉樹結構設置相關的方法,例如添加節點、設置數據、刪除節點等等。

另外需要注意的是,一定不要將某個新節點的左、右同時設置爲樹中已存在的節點,因爲這樣會讓樹結構封閉起來,這會破壞了二叉樹的結構。


版權申明:內容來源網絡,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章