一、struct簡介
go語言中沒有像類的概念,但是可以通過結構體struct實現oop(面向對象編程)。struct的成員(也叫屬性或字段)可以是任何類型,如普通類型、復合類型、函數、map、interface、struct等,所以我們可以理解為go語言中的“類”。
二、struct詳解
struct定義
在定義struct成員時候區分大小寫,若首字母大寫則該成員為公有成員(對外可見),否則是私有成員(對外不可見)。
type struct_variable_type struct { member member_type member member_type ..... member member_type } //示例 type Student struct { name string age int Class string }
聲明與初始化
var stu1 Student var stu2 *Student= &Student{} //簡寫stu2 := &Student{} var stu3 *Student = new(Student) //簡寫stu3 := new(Student)
struct使用
在struct中,無論使用的是指針的方式聲明還是普通方式,訪問其成員都使用".",在訪問的時候編譯器會自動把 stu2.name 轉為 (*stu2).name。
struct分配內存使用new,返回的是指針。
struct沒有構造函數,但是我們可以自己定義“構造函數”。
struct是我們自己定義的類型,不能和其他類型進行強制轉換。
package main import "fmt" type Student struct { name string age int Class string } func main() { var stu1 Student stu1.age = 22 stu1.name = "wd" stu1.Class = "class1" fmt.Println(stu1.name) //wd var stu2 *Student = new(Student) stu2.name = "jack" stu2.age = 33 fmt.Println(stu2.name,(*stu2).name)//jack jack var stu3 *Student = &Student{ name:"rose",age:18,Class:"class3"} fmt.Println(stu3.name,(*stu3).name) //rose rose }
自定義構造函數
以下是通過工廠模式自定義構造函數方法
package main import "fmt" type Student struct { name string age int Class string } func Newstu(name1 string,age1 int,class1 string) *Student { return &Student{name:name1,age:age1,Class:class1} } func main() { stu1 := Newstu("wd",22,"math") fmt.Println(stu1.name) // wd }
tag
tag可以為結構體的成員添加說明或者標簽便於使用,這些說明可以通過反射獲取到。
在前面提到了,結構體中的成員首字母小寫對外不可見,但是我們把成員定義為首字母大寫這樣與外界進行數據交互會帶來極大的不便,此時tag帶來了解決方法。
type Student struct { Name string "the name of student" Age int "the age of student" Class string "the class of student" }
應用場景示例,json序列化操作:
package main import ( "encoding/json" "fmt" ) type Student struct { Name string `json:"name"` Age int `json:"age"` } func main() { var stu = Student{Name:"wd",Age:22} data,err := json.Marshal(stu) if err != nil{ fmt.Println("json encode failed err:",err) return } fmt.Println(string(data)) //{"name":"wd","age":22} }
匿名成員(字段、屬性)
結構體中,每個成員不一定都有名稱,也允許字段沒有名字,即匿名成員。
匿名成員的一個重要作用,可以用來實現oop中的繼承。
同一種類型匿名成員只允許最多存在一個。
當匿名成員是結構體時,且兩個結構體中都存在相同字段時,優先選擇最近的字段。
package main import "fmt" type Person struct { Name string Age int } type Student struct { score string Age int Person } func main() { var stu = new(Student) stu.Age = 22 //優先選擇Student中的Age fmt.Println(stu.Person.Age,stu.Age)// 0,22 }
繼承、多繼承
當結構體中的成員也是結構體時,該結構體就繼承了這個結構體,繼承了其所有的方法與屬性,當然有多個結構體成員也就是多繼承。
訪問父結構中屬性也使用“.”,但是當子結構體中存在和父結構中的字段相同時候,只能使用:"子結構體.父結構體.字段"訪問父結構體中的屬性,如上面示例的stu.Person.Age
繼承結構體可以使用別名,訪問的時候通過別名訪問,如下面示例man1.job.Salary:
package main import "fmt" type Person struct { Name string Age int } type Teacher struct { Salary int Classes string } type man struct { sex string job Teacher //別名,繼承Teacher Person //繼承Person } func main() { var man1 = new(man) man1.Age = 22 man1.Name = "wd" man1.job.Salary = 8500 fmt.Println(man1,man1.job.Salary) //&{ {8500 } {wd 22}} 8500 }
結構體中的方法
go語言中的方法是作用在特定類型的變量上,因此自定義的類型都可以有方法,不僅僅是在結構體中。
go中的方法和傳統的類的方法不太一樣,方法和類並非組織在一起,傳統的oop方法和類放在一個文件里面,而go語言只要在同一個包里就可,可分散在不同文件里。go的理念就是數據和實現分離,引用官方說法:“Methods are not mixed with the data definition (the structs): they are orthogonal to types; representation(data) and behavior (methods) are independent”
方法的調用通過recv.methodName(),其訪問控制也是通過大小寫區分。
方法定義,其中recv代表方法作用的結構體:
func (recv type) methodName(parameter_list) (return_value_list) { … }
package main import "fmt" type Person struct { Name string Age int } func (p Person) Getname() string{ //p代表結構體本身的實列,類似python中的self,這里p可以寫為self fmt.Println(p.Name) return p.Name } func main() { var person1 = new(Person) person1.Age = 22 person1.Name = "wd" person1.Getname()// wd }
當有了結構的方法時候,我們可以自己定義其初始化方法,由於結構體是值類型,所以我們使用指針才能改變其存儲的值。
package main import "fmt" type Person struct { Name string Age int } func (self *Person) init(name string ,age int){ self.Name = name self.Age = age } func main() { var person1 = new(Person) person1.init("wd",22) //(&person1).init("wd",22) fmt.Println(person1)//&{wd 22} }
如果實現了結構體中的String方法,在使用fmt打印時候會調用該方法,類似與python中的__str__方法.
package main import "fmt" type Person struct { Name string Age int } func (self *Person) String() string{ return self.Name } func main() { var person1 = new(Person) person1.Name = "wd" person1.Age = 22 fmt.Println(person1)// wd }
內存分布
go中的結構體內存布局和c結構體布局類似,每個成員的內存分布是連續的,在以下示例中通過反射進行進一步說明:
package main import ( "fmt" "reflect" ) type Student struct { Name string Age int64 wight int64 high int64 score int64 } func main() { var stu1 = new(Student) fmt.Printf("%p\n",&stu1.Name) fmt.Printf("%p\n",&stu1.Age) fmt.Printf("%p\n",&stu1.wight) fmt.Printf("%p\n",&stu1.high) fmt.Printf("%p\n",&stu1.score) typ := reflect.TypeOf(Student{}) fmt.Printf("Struct is %d bytes long\n", typ.Size()) // We can run through the fields in the structure in order n := typ.NumField() for i := 0; i < n; i++ { field := typ.Field(i) fmt.Printf("%s at offset %v, size=%d, align=%d\n", field.Name, field.Offset, field.Type.Size(), field.Type.Align()) } } //結果 0xc42007a180 0xc42007a190 0xc42007a198 0xc42007a1a0 0xc42007a1a8 Struct is 48 bytes long Name at offset 0, size=16, align=8 Age at offset 16, size=8, align=8 wight at offset 24, size=8, align=8 high at offset 32, size=8, align=8 score at offset 40, size=8, align=8
在以上結果中,可以看到內存地址的偏移總是以8字節偏移(使用的是int64,剛好是8字節),在觀察其內存地址,也是連續的,所以go語言中的結構體內存布局是連續的。如下圖:

三、使用struct實現鏈表
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。
鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。
下面以單鏈表為例,使用go語言實現:
單鏈表
單鏈表:每個節點包含下一個節點的地址,這樣把所有節點都串起來的鏈式數據數據結構叫做鏈表,通常把鏈表中的第一個節點叫做表頭。
使用struct定義單鏈表:
為了方便,數據區域這里使用int
type Node struct { data int next *node }
鏈表遍歷
鏈表的遍歷是通過移動指針進行遍歷,當指針到最好一個節點時,其next指針為nil
package main import "fmt" type Node struct { data int next *Node } func Shownode(p *Node){ //遍歷 for p != nil{ fmt.Println(*p) p=p.next //移動指針 } } func main() { var head = new(Node) head.data = 1 var node1 = new(Node) node1.data = 2 head.next = node1 var node2 = new(Node) node2.data = 3 node1.next = node2 Shownode(head) } //{1 0xc42000e1e0} //{2 0xc42000e1f0} //{3 <nil>}
插入節點
單鏈表的節點插入方法一般使用頭插法或者尾插法。
頭插法:每次插入在鏈表的頭部插入節點。
package main import "fmt" type Node struct { data int next *Node } func Shownode(p *Node){ //遍歷 for p != nil{ fmt.Println(*p) p=p.next //移動指針 } } func main() { var head = new(Node) head.data = 0 var tail *Node tail = head //tail用於記錄頭節點的地址,剛開始tail的的指針指向頭節點 for i :=1 ;i<10;i++{ var node = Node{data:i} node.next = tail //將新插入的node的next指向頭節點 tail = &node //重新賦值頭節點 } Shownode(tail) //遍歷結果 } //{9 0xc42007a240} //{8 0xc42007a230} //{7 0xc42007a220} //{6 0xc42007a210} //{5 0xc42007a200} //{4 0xc42007a1f0} //{3 0xc42007a1e0} //{2 0xc42007a1d0} //{1 0xc42007a1c0} //{0 <nil>}
尾插法:每次插入節點在尾部,這也是我們較為習慣的方法。
package main import "fmt" type Node struct { data int next *Node } func Shownode(p *Node){ //遍歷 for p != nil{ fmt.Println(*p) p=p.next //移動指針 } } func main() { var head = new(Node) head.data = 0 var tail *Node tail = head //tail用於記錄最末尾的節點的地址,剛開始tail的的指針指向頭節點 for i :=1 ;i<10;i++{ var node = Node{data:i} (*tail).next = &node tail = &node } Shownode(head) //遍歷結果 } //{0 0xc42007a1c0} //{1 0xc42007a1d0} //{2 0xc42007a1e0} //{3 0xc42007a1f0} //{4 0xc42007a200} //{5 0xc42007a210} //{6 0xc42007a220} //{7 0xc42007a230} //{8 0xc42007a240} //{9 <nil>}
