第四章 复合数据类型
基础数据类型是Go语言世界的原子
复合数据类型包括四种:slice、map、 struct、array
数组和结构体是聚合类型,它们的值由许多元素或成员构成,数组和结构体都是固定内存大小的数据结构,,相比之下,slice和map则是动态的数据结构,它们将根据动态增长
4.4 结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体,每个值称为结构体的成员
结构体及结构体类型变量的声明
我们定一个结构体处理公司的员工信息,结构体成员的类型相同可以合并到一行
type Employee struct { //结构体
ID int
Name string
Address,Position string
DoB time.Time
Salary int
ManagerID int
}
var dilbert Employee //结构体类型变量
结构体成员的访问和操作
结构体中的成员可以通过结构体类型的变量加点 . 操作符进行访问(也可以直接赋值)
dilbert.Salary = 5000
fmt.Println(dilbert.Salary)//5000
对成员取地址
position := &dilbert.Position
*position = "Senior" + *position
fmt.Println(*position)//Senior
指向结构体
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += "(proactive team player)"
//等价于 (*employeeOfTheMonth).Position += "(proactive team player)"
fmt.Println(&employeeOfTheMonth.Position)//0xc0000561c0
结构体成员通过函数访问
点 . 操作符不仅可以放在结构体类型的变量后面访问结构体成员,还可以放在函数后面访问结构体成员,而且它可以实现两层意思,如某个id(员工)的薪水,注意,函数的返回值必须是 *Employee指针类型
func EmployeeByID(id int) *Employee {
fmt.Println(EmployeeByID(dilbert.ManagerID).Position)
id := dilbert.ID
EmployeeByID(id).Salary = 0
}
结构体类型的注意点
一个命名为S的结构体类型不可以再包含S类型的成员:因为一个聚合的值不能包含它自身(该限制同样适用于数组),但是可以包含*s指针类型成员,这样我们可以创建递归的数据结构,比如链表和树结构
我们用一个二叉树来实现插入排序
type tree struct {
value int
left, right *tree//本结构体的指针类型
}
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root,v)
}
appendValues(values[:0],root)
}
func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
t = new(tree)
t.value = value
return t
}
if value < t.value {
t.left = add(t.left,value)
}else {
t.right = add(t.right,value)
}
return t
}
结构体类型的零值是每个成员都是零值,通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,sync.Mutex的零值是有效的未锁定状态
结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,不包含任何信息,但有时候依然有价值。我们在使用map来模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为空间节约有限,而且语法比较复杂,所以我们通常会避免使用这样的方法
seen := make(map[string]struct{})
if _,ok := seen[s]; !ok {
seen[s] = struct{}{}
}
4.4.1 结构体字面值
结构体成员赋值可以用字面值直接表示,但是如下这种方式就需要记住结构体成员的定义顺序
type Point struct {
x,y int
}
p := Point{1,2}
在日常中,我们更常用的是以成员的名字和相应的值来初始化,使用这种方式就可以包含全部或者部分成员,定义的顺序并不重要
anim := gif.GIF{LoopCount: nframes}
结构体可以作为函数的参数和返回值,如果在函数内步修改结构体成员的话,永指针传入是必须的,在Go语言中,所有函数参数都是拷贝传入的,函数参数将不再是函数调用时的原始变量
type Point struct {
X,Y int
}
func Scale(p Point,factor int) Point{
return Point{p.X *factor,p.Y *factor}
}
func main() {
fmt.Println(Scale(Point{1,2},5))//{5 10}
}
如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回
func Bonus (e *Employee,percent int) int {
return e.Salary * percent / 100
}
在函数内部修改成员,必须指针传入
func AwardAnnualRaise(e *Employee) {
e.Salary = e.Salary * 105 / 100
}
因为结构体通常通过指针来处理,用如下写法来创建并初始化一个结构体变量,并返回结构体地址,并且&Point{1,2}写法可以直接在表达式中使用,比如一个函数调用
pp := &Point{1,2}
fmt.Println(pp)//&{1 2}
等价于如下
pp1 := new(Point)
*pp1 = Point{1, 2}
fmt.Println(pp1,*pp1) //&{1 2} {1 2}
4.4.2 结构体的比较
如果结构体成员全都是可以比较的,那么结构体也是可以比较的,比较用 == or !=
p := Point{1,2}
q := Point{2,1}
fmt.Println(p.X == q.X && p.Y == q.Y)// false
fmt.Println(p == q)// false
另外,可比较的结构体和其他可比较结的类型一样,可以用于做map的key
type address struct {
hostname string
port int
}
hits := make(map[address]int)
hits[address{"golang.org",443}] ++
4.4.3 结构体嵌入和匿名成员
本节中我们来一起了解一下结构体的嵌入机制,这样就可以使用点运算符 x.f 来访问匿名成员链中嵌套的x.d.e.f 成员
我们来考虑一个绘图程序:
type Circle struct {
X, Y, Radius int
}
type Wheel struct {
X, Y, Radius, Spokes int
}
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
但是我们发现Circle 和 Wheel这两个结构体有些共同属性,那我们可以把它们提取出来,也易于后续的维护,但同时,访问会变得更加繁琐,访问顺序,从最大的结构体一层一层往里剥
type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.X = 8
w.Circle.Radius = 5
w.Spokes = 20
匿名成员,只有成员类型,而没有成员名称,成员的类型必须是命名类型或者指向一个命名类型的指针,可以是任何命名类型,但是不能包含两个相同类型的匿名成员
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出它的完整路径,但是在包外,如果匿名成员不是导出的话就不可以以简短形式访问
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
结构体字面值并没有简短表示匿名成员的方法,所以只能用点 . 操作符或者是下面这种方式
//w = Wheel{8,8,5,20}
//w =Wheel{X:8,Y:8,Radius:5,Spokes: 20}
w = Wheel{Circle:Circle{
Point:Point{X: 8,Y: 8},
Radius: 5,
},
Spokes: 20,
}
fmt.Println(w)//{{{8 8} 5} 20}
在实际中,我们会嵌入一个没有任何子成员的匿名成员类型,为什么呢?答案是匿名类型的方法集
简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问他们的方法,实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在下一节讲到