struct定義結構,結構由字段(field)組成,每一個field都有所屬數據類型,在一個struct中,每一個字段名都必須惟一。node
說白了就是拿來存儲數據的,只不過可自定義化的程度很高,用法很靈活,Go中很多功能依賴於結構,就這樣一個角色。數據結構
Go中不支持面向對象,面向對象中描述事物的類的重擔由struct來挑。好比面向對象中的繼承,可使用組合(composite)來實現:struct中嵌套一個(或多個)類型。面向對象中父類與子類、類與對象的關係是is a
的關係,例如Horse is a Animal
,Go中的組合則是外部struct與內部struct的關係、struct實例與struct的關係,它們是has a
的關係。Go中經過struct的composite,能夠"模仿"不少面向對象中的行爲,它們很"像"。ide
定義struct的格式以下:函數
type identifier struct { field1 type1 field2 type2 … } // 或者 type T struct { a, b int }
理論上,每一個字段都是有具備惟一性的名字的,但若是肯定某個字段不會被使用,能夠將其名稱定義爲空標識符_
來丟棄掉:spa
type T struct { _ string a int }
每一個字段都有類型,能夠是任意類型,包括內置簡單數據類型、其它自定義的struct類型、當前struct類型自己、接口、函數、channel等等。指針
若是某幾個字段類型相同,能夠縮寫在同一行:code
type mytype struct { a,b int c string }
定義了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{}
的方式。
下面三種方式均可以構造person struct的實例p:
p1 := person{} p2 := &person{} p3 := new(person)
但p1和p二、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}
p一、p二、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)的地址。一般,將指向指針的變量(p一、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}
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屬性。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中的字段能夠不用給名稱,這時稱爲匿名字段。匿名字段的名稱強制和類型相同。例如:
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
。
輸出如下外部struct的內容就很清晰了:
fmt.Println(o) // 結果:&{1 2 3 {4 5}}
上面的outer實例,也能夠直接賦值構建:
o := outer{1,2,3,inner{4,5}}
在賦值inner中的in1和in2時不能少了inner{}
,不然會認爲in一、in2是直接屬於outer,而非嵌套屬於outer。
顯然,struct的嵌套相似於面向對象的繼承。只不過繼承的關係模式是"子類 is a 父類",例如"轎車是一種汽車",而嵌套struct的關係模式是外部struct has a 內部struct
,正如上面示例中outer擁有inner
。並且,從上面的示例中能夠看出,Go是支持"多重繼承"的。
前面所說的是在struct中以匿名的方式嵌套另外一個struct,但也能夠將嵌套的struct帶上名稱。
直接帶名稱嵌套struct時,不會再自動深刻到嵌套struct中去查找屬性和方法。想要訪問內部struct屬性時,必須帶上該struct的名稱。
例如:
type animal struct { name string age int } type Horse struct{ a animal sound string }
這時候,想要訪問嵌套在Horse中animal的name屬性,則只能經過h.a.name
的方式(h爲Horse的實例對象),且訪問h.name
時將直接報錯,由於在Horse裏找不到name屬性。
假如外部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。
若是struct中嵌套的struct類型是本身的指針類型,能夠用來生成特殊的數據結構:鏈表或二叉樹(雙端鏈表)。
例如,定義一個單鏈表數據結構,每一個Node都指向下一個Node,最後一個Node指向空。
type Node struct { data string ri *Node }
如下是鏈表結構示意圖:
------|---- ------|---- ------|----- | data | ri | --> | data | ri | --> | data | nil | ------|---- ------|---- ------|-----
若是給嵌套兩個本身的指針,每一個結構都有一個左指針和一個右指針,分別指向它的左邊節點和右邊節點,就造成了二叉樹或雙端鏈表數據結構。
二叉樹的左右節點能夠留空,可隨時向其中加入某一邊加入新節點(像節點加入到樹中)。添加節點時,節點與節點之間的關係是父子關係。添加完成後,節點與節點之間的關係是父子關係或兄弟關係。
雙端鏈表有所不一樣,添加新節點時必須讓某節點的左節點和另外一個節點的右節點關聯。例如目前已有的鏈表節點A <-> C
,如今要將B節點加入到A和C的中間,即A<->B<->C
,那麼A的右節點必須設置爲B,B的左節點必須設置爲A,B的右節點必須設置爲C,C的左節點必須設置爲B。也就是涉及了4次原子性操做,它們要麼全設置成功,失敗一個則鏈表被破壞。
例如,定義一個二叉樹:
type Tree struct { le *Tree data string ri *Tree }
最初生成二叉樹時,root節點沒有任何指向。
// root節點:初始左右兩端爲空 root := new(Tree) root.data = "root node"
隨着節點增長,root節點開始指向其它左節點、右節點,這些節點還能夠繼續指向其它節點。向二叉樹中添加節點的時候,只需將新生成的節點賦值給它前一個節點的le或ri字段便可。例如:
// 生成兩個新節點:初始爲空 newLeft := new(Tree) newLeft.data = "left node" newRight := &Tree{nil, "Right node", nil} // 添加到樹中 root.le = newLeft root.ri = newRight // 再添加一個新節點到newLeft節點的右節點 anotherNode := &Tree{nil, "another Node", nil} newLeft.ri = anotherNode
簡單輸出這個樹中的節點:
fmt.Println(root) fmt.Println(newLeft) fmt.Println(newRight)
輸出結果:
&{0xc042062400 root node 0xc042062420} &{<nil> left node 0xc042062440} &{<nil> Right node <nil>}
固然,使用二叉樹的時候,必須爲二叉樹結構設置相關的方法,例如添加節點、設置數據、刪除節點等等。
另外須要注意的是,必定不要將某個新節點的左、右同時設置爲樹中已存在的節點,由於這樣會讓樹結構封閉起來,這會破壞了二叉樹的結構。