一、struct简介
- go语言中没有像类的概念,但是可以通过结构体struct实现oop(面向对象编程)。struct的成员(也叫属性或字段)可以是任何类型,如普通类型、复合类型、函数、map、interface、struct等,所以我们可以理解为go语言中的“类”。
二、struct详解
- struct定义
在定义struct成员时候区分大小写,若首字母大写则该成员为公有成员(对外可见),否则是私有成员(对外不可见)。
//示例
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 = 28
stu1.name = "Jack Wong"
stu1.Class = "Class one"
fmt.Println(stu1.name) //Jack Wong
var stu2 *Student = new(Student)
stu2.name = "Tom Wong"
stu2.age = 35
fmt.Println(stu2.name, (*stu2).name) //Tom Wong
var stu3 *Student = &Student{name: "Jhon Wong", age: 18, Class: "class two"}
fmt.Println(stu3.name, (*stu3).name)
}
- 自定义构造函数
以下是通过工厂模式自定义构造函数方法
package main
import "fmt"
type Student struct {
name string
age int
Class string
}
func NewStudent(name1 string, age1 int, Class1 string) *Student {
return &Student{name: name1, age: age1, Class: Class1}
}
func main() {
stu1 := NewStudent("Jack Tom", 22, "math class")
fmt.Println(stu1.name)
}
- tag
tag可以为结构体的成员添加说明或者标签便于使用,这些说明可以通过反射获取到。在前面提到了,结构体中的成员首字母小写对外不可见,但是我们把成员定义为首字母大写这样与外界进行数据交互会带来极大的不便,此时tag带来了解决方法。
type Student struct {
Name string "the name of student"
Age int "the age of student"
Class string "the class of student"
}
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Class string `json:"class"`
}
func main() {
var stu = Student{Name: "Wong", Age: 28, Class: "Class One"}
data, err := json.Marshal(stu)
if err != nil {
fmt.Println("json encode failed err :", err)
}
fmt.Println(string(data))
}
- 匿名成员(字段、属性)
结构体中,每个成员不一定都有名称,也允许字段没有名字,即匿名成员。
匿名成员的一个重要作用,可以用来实现oop中的继承。
同一种类型匿名成员只允许最多存在一个。
当匿名成员是结构体时,且两个结构体中都存在相同字段时,优先选择最近的字段。
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
type Student struct {
Name string
Age int
Person
}
func main() {
var stu = new(Student)
stu.Age = 28 //优先选择Student中的Age
fmt.Println(stu.Person.Age, stu.Age) //0, 28
}
- 继承、多继承
当结构体中的成员也是结构体时,该结构体就继承了这个结构体,继承了其所有的方法与属性,当然有多个结构体成员也就是多继承。访问父结构中属性也使用“.”,但是当子结构体中存在和父结构中的字段相同时候,只能使用:"子结构体.父结构体.字段"访问父结构体中的属性,如上面示例的stu.Person.Age继承结构体可以使用别名,访问的时候通过别名访问,如下面示例man1.job.Salary:
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
type Teacher struct {
Salary int
Class string
}
type man struct {
sex string
job Teacher //别名,继承Teacher
Person
}
func main() {
var man1 = new(man)
man1.Age = 28
man1.job.Salary = 16000
fmt.Println(man1, man1.job.Salary) //输出&{ {16000 } { 28}} 16000
}
结构体中的方法
o语言中的方法是作用在特定类型的变量上,因此自定义的类型都可以有方法,不仅仅是在结构体中。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 {
fmt.Println(p.Name)
return p.Name
}
func main() {
var person1 = new(Person)
person1.Age = 28
person1.Name = "Jack Wong"
person1.Getname() //Jack Wong
}
当有了结构的方法时候,我们可以自己定义其初始化方法,由于结构体是值类型,所以我们使用指针才能改变其存储的值
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("Jack Wong", 22)
fmt.Println(person1) //&{Jack Wong 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.Age = 28
person1.Name = "Jack Wong"
fmt.Println(person1) //Jack Wong
}
- 内存分布
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())
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())
}
}
输出结果:
0xc00006e330
0xc00006e340
0xc00006e348
0xc00006e350
0xc00006e358
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实现链表
单链表的原理在c++章节中有过重点介绍,现在主要讲它go语言代码实现
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。链表有很多种不同的类型:单向链表,双向链表以及循环链表。下面以单链表为例,使用go语言实现:
package main
import "fmt"
//创建节点结构/类型
type Node struct {
Data interface{}
Next *Node
}
//创建链表结构
type List struct {
Head *Node
Length int //这里的链表长度不计入节点头
}
//接口设计
type Method interface {
Insert(i int, v interface{}) //增
Delete(i int) //删
GetLength() int //获取长度
Search(v interface{}) int //查
isEmpty() bool //判断是否为空
}
//初始化函数
//创建节点
func CreateNode(v interface{}) *Node {
return &Node{v, nil}
}
//创建空链表
func CreateList() *List {
return &List{CreateNode(nil), 0}
}
//基于链表结构体实现接口Method中的方法
//即插入原来的第i个节点之前,成为现在的第i个节点
func (list *List) Insert(i int, v interface{}) {
s := CreateNode(v)
pre := list.Head
for count := 0; count <= i; count++ {
if count == i-1 {
s.Next = pre.Next
pre.Next = s
list.Length++
}
pre = pre.Next
}
}
//删除第i个节点
func (list *List) Delete(i int) {
pre := list.Head
for count := 0; count <= i-1; count++ {
s := pre.Next
if count == i-1 {
pre.Next = s.Next
list.Length--
}
pre = pre.Next
}
}
//返回链表长度
func (list *List) GetLength() int {
return list.Length
}
//查询值v所在的位置
func (list *List) Search(v interface{}) int {
pre := list.Head.Next
for i := 1; i <= list.Length; i++ {
if pre.Data == v {
return i
}
pre = pre.Next
}
return 0
}
//判断是否为空
func (list *List) isEmpty() bool {
pre := list.Head.Next
if pre == nil {
return true
}
return false
}
//设计链表打印输出
func PrintList(list *List) {
pre := list.Head.Next
fmt.Println("List show as follow....\n")
for i := 1; i < list.Length; i++ {
fmt.Printf("%v", pre.Data)
pre = pre.Next
}
}
func main() {
list := CreateList()
fmt.Println("List is Null:", list.isEmpty())
var M Method
M = list //接口类型变量可以存储所有实现该类型接口的变量
M.Insert(1, 3)
M.Insert(2, 6)
M.Insert(1, 5)
PrintList(list)
fmt.Println("List Legth is :", list.Length)
fmt.Println("元素6在位置:", M.Search(6))
fmt.Println("元素100在位置:", M.Search(100))
fmt.Println("List is NULL:", list.isEmpty())
M.Delete(2)
PrintList(list)
fmt.Println("List length is:", list.Length)
}
输出结果:
List is Null: true
List show as follow....
53List Legth is : 3
元素6在位置: 3
元素100在位置: 0
List is NULL: false
List show as follow....
5List length is: 2