一, 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>}
来源: https://www.cnblogs.com/wdliu/p/9209419.html