go语言之行--结构体(struct)详解、链表
一、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>}