go-结构体

结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型

类型定义

最常用的方法是使用关键字struct来创建一个结构

1
2
3
4
5
6
7
type User struct {
Name string
Email string
Ext int
Privileged bool
}
var bill User // 声明User类型的变量,同时做了零值的初始化(对于数值类型,零值是0;对于字符串,零值是空字符串,对于布尔型,零值是false)

成员变量的顺序对于结构体同一性很重要。如果我们将也是字符串类型的Name和Address组合在一起或者互换了Name和Email的顺序,那么我们就在定义一个不同的结构体类型。一般来讲,我们只组合相关的成员变量

注意: 任何时候,创建一个变量并初始化为其零值,习惯是使用关键字var。这种用法是为了更明确地表示一个变量被设置为零值。如果变量被初始化为某个非零值,就配置结构字面量和短变量声明操作符来创建变量:

1
2
3
4
5
6
7
8
9
// 短变量声明User类型的变量,并初始化所有字段
lisa := User{
Name: "Lisa",
Email: "lisa@email.com",
Ext: 123,
Privileged: true,
}
// 声明User类型的变量,这个写在一行的方式,结尾没有逗号。值的顺序很重要,必须要和结构声明中字段的顺序一致
lisa := User{"Lisa", "lisa@email.com", 123, true}

1
2
3
4
5
6
7
// 结构字面量使用一堆大括号扩住内部字段的初始值来创建并初始化结构类型
User{
Name: "List",
Email: "lisa@email.com",
Ext: 123,
Privileged: true,
}
1
2
3
4
5
6
7
package p
type T struct{a, b int} // a和b小写,不可导出
package q
import "p"
var _ = p.T{a: 1, b: 2} // 编译错误,无法引用a、b
var _ = p.T{1, 2} // 编译错误,无法引用a、b

出于效率考虑,大型的结构体通常都使用结构体指针的方式直接传递给函数或者从函数中返回:

1
2
3
func Bonus(e *Employee, percent int) int {
return e.Salary * percent / 100
}

Go语言是按值调用,调用的函数接收到的是实参的一个副本,并不是实参的引用。

如果结构体的所有成员变量都可以比较,那么这个结构体就是可比较的。两个结构体的比较可以使用 == 或者 !=。其中 == 操作符按照顺序比较两个结构体变量的成员变量,所以下面的两个输出语句是等价的:

1
2
3
4
5
type Point struct{X, Y int}
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的键类型:

1
2
3
4
5
6
type address struct {
hostname string
port int
}
hits := make(map[address]int)
hits[address{"golang.com", 443}]++

结构体嵌套和匿名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}

变量访问:

1
2
3
4
5
var w Wheel
w.X = 8 // 等价于w.Circle.Point.X = 8
w.Y = 8 // 等价于w.Circle.Point.Y = 8
w.Radius = 5 // 等价于w.Circle.Radius = 5
w.Spokes = 20

由于结构体字面量并没有什么快捷方式来初始化结构体,以下语句会编译错误:

1
2
w = Wheel{8, 8, 5, 20} // 编译错误,未知成员变量
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // 编译错误,未知成员变量

结构体字面量必须遵循形状类型的定义, 所以下面两种方式初始化是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
w = Wheel{Circel{Point{8, 8}, 5}, 20}
// 等价
w = Wheel{
Circel: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // 注意,尾部的逗号是必须的(Radius后面的逗号也一样)
}
fmt.Printf("%#v\n", w) // 输出:Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.X = 42
fmt.Printf("%#v\n", w) // 输出:Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}

这里注意副词#如何使得Printf的格式化符号%v以类似Go语法的方式输出对象,这个方式里包含了成员变量的名字

任何时候,创建一个变量并初始化为其零值,习惯是使用关键字var。这种用法是为了更明确地表示一个变量被设置为零值。如果变量被初始化为某个非零值,就配合结构字面量和短变量声明操作符来创建变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type user struct {
name string
email string
}
type admin struct {
user // 嵌入类型
level string
}
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}
func main() {
ad := admin{
user: user{
name: "layne",
email: "layne@email.com",
},
level: "super",
}
ad.user.notify() // 可以直接访问内部类型的方法
ad.notify() // 内部类型的方法也被提升到外部类型
}
同样,如果内部类型有实现接口,那么该接口也会被提升到外部接口。如果外部类型同样实现了该接口,则内部类型的接口不会被提升到外部。不过我们可以通过嵌套访问内部类型的接口

类型声明二

另一种声明用户定义的类型方法是,基于一个已有的类型,将其作为新类型的类型说明:

1
type Duration int64 // 这个类型使用内置的int64类型作为其表示, 我们把int64类型叫做Duration的基础类型

Go并不认为Duration和int64是同一种类型。这两个类型是完全不同的有区别的类型:

1
2
3
4
5
6
type Duration int64 // 声明类型 Duration
func main() {
var dur Duration // 声明一个类型为Duration的变量cur,并使用零值作为初值
dur = int64(1000) // 编译出错, 类型int64的值不能作为类型Duration的值来用。换句话说,虽然int64类型是基础类型,Duration类型依然是一个独立的类型。两种不同类型的值几遍互相兼容,也不能互相赋值。编译器不会对不同类型的值做隐式转换
}

小结

  • 使用关键字struct或者通过指定一家存在的类型,可以声明用户定义的类型
  • 方法提供了一种给用户定义的类型增加行为的方式
  • 设计类型时需要确认类型的本质是原始的,还是非原始的
  • 接口是声明了一组行为并支持多台的类型
  • 嵌入类型提供了扩展类型的功能,而无需使用继承
  • 标识符要么是从包里面公开的,要么是在包里未公开的