10.1 结构体定义
结构体定义的一般方式如下:
type identifier struct {
field1 type1
field2 type2
...
}
type T struct {a, b int}
也是合法的语法,它更适用于简单的结构体。
结构体里的字段都有 名字,像 field1
、field2
等,如果字段在代码中从来也不会被用到,那么可以命名它为 _
。
结构体的字段可以是任何类型,甚至是结构体本身(参考第 10.5 节),也可以是函数或者接口(参考第 11 章)。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:
var s T
s.a = 5
s.b = 8
数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。
使用 new()
使用 new()
函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T)
,如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。
var t *T
t = new(T)
写这条语句的惯用方法是:t := new(T)
,变量 t
是一个指向 T
的指针,此时结构体字段的值是它们所属类型的零值。
声明 var t T
也会给 t
分配内存,并零值化内存,但是这个时候 t
是类型 T
。在这两种方式中,t
通常被称做类型 T 的一个实例 (instance) 或对象 (object)。
示例 10.1 structs_fields.go 给出了一个非常简单的例子:
package main
import "fmt"
type struct1 struct {
i1 int
f1 float32
str string
}
func main() {
ms := new(struct1)
ms.i1 = 10
ms.f1 = 15.5
ms.str= "Chris"
fmt.Printf("The int is: %d\n", ms.i1)
fmt.Printf("The float is: %f\n", ms.f1)
fmt.Printf("The string is: %s\n", ms.str)
fmt.Println(ms)
}
输出:
The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}
使用 fmt.Println()
打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v
选项。
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:structname.fieldname = value
。
同样的,使用点号符可以获取结构体字段的值:structname.fieldname
。
在 Go 语言中这叫 选择器 (selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符 (selector-notation) 来引用结构体的字段:
type myStruct struct { i int }
var v myStruct // v 是结构体类型变量
var p *myStruct // p 是指向一个结构体类型变量的指针
v.i
p.i
初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:
ms := &struct1{10, 15.5, "Chris"}
// 此时 ms 的类型是 *struct1
或者:
var ms struct1
ms = struct1{10, 15.5, "Chris"}
混合字面量语法 (composite literal syntax) &struct1{a, b, c}
是一种简写,底层仍然会调用 new()
,这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 new(Type)
和 &Type{}
是等价的。
时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:
type Interval struct {
start int
end int
}
初始化方式:
intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)
在 (A) 中,值必须以字段在结构体定义时的顺序给出,&
不是必须的。(B) 显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像 (C) 中那样。
结构体类型和字段的命名遵循可见性规则(第 4.2 节),一个导出的结构体类型中有些字段是导出的,另一些不是,这是可能的。
下图说明了结构体类型实例和一个指向它的指针的内存布局:
type Point struct { x, y int }
使用 new()
初始化:
作为结构体字面量初始化:
类型 struct1
在定义它的包 pack1
中必须是唯一的,它的完全类型名是:pack1.struct1
。
下面的例子 Listing 10.2—person.go 显示了一个结构体 Person
,一个方法 upPerson()
,方法有一个类型为 *Person
的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:
package main
import (
"fmt"
"strings"
)
type Person struct {
firstName string
lastName string
}
func upPerson(p *Person) {
p.firstName = strings.ToUpper(p.firstName)
p.lastName = strings.ToUpper(p.lastName)
}
func main() {
// 1-struct as a value type:
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Woodward"
upPerson(&pers1)
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)
// 2—struct as a pointer:
pers2 := new(Person)
pers2.firstName = "Chris"
pers2.lastName = "Woodward"
(*pers2).lastName = "Woodward" // 这是合法的
upPerson(pers2)
fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)
// 3—struct as a literal:
pers3 := &Person{"Chris","Woodward"}
upPerson(pers3)
fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}
输出:
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
在上面例子的第二种情况中,可以直接通过指针,像 pers2.lastName = "Woodward"
这样给结构体字段赋值,没有像 C++ 中那样需要使用 ->
操作符,Go 会自动做这样的转换。
注意也可以通过解指针的方式来设置值:(*pers2).lastName = "Woodward"
结构体的内存布局
Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。下面的例子清晰地说明了这些情况:
type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }
递归结构体
结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)。如下所示,链表中的 su
,树中的 ri
和 le
分别是指向别的节点的指针。
链表:
这块的 data
字段用于存放有效数据(比如 float64
),su
指针指向后继节点。
Go 代码:
type Node struct {
data float64
su *Node
}
链表中的第一个元素叫 head
,它指向第二个元素;最后一个元素叫 tail
,它没有后继元素,所以它的 su
为 nil
值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。
同样地可以定义一个双向链表,它有一个前趋节点 pr
和一个后继节点 su
:
type Node struct {
pr *Node
data float64
su *Node
}
二叉树:
二叉树中每个节点最多能链接至两个节点:左节点 (le
) 和右节点 (ri
),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点 (root),底层没有子节点的节点叫叶子节点 (leaves),叶子节点的 le
和 ri
指针为 nil
值。在 Go 中可以如下定义二叉树:
type Tree struct {
le *Tree
data float64
ri *Tree
}
结构体转换
Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias
类型时,此结构体类型和它的 alias
类型都有相同的底层类型,它们可以如示例 10.3 那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。
示例 10.3:
package main
import "fmt"
type number struct {
f float32
}
type nr number // alias type
func main() {
a := number{5.0}
b := nr{5.0}
// var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment
// var i = float32(b) // compile-error: cannot convert b (type nr) to type float32
// var c number = b // compile-error: cannot use b (type nr) as type number in assignment
// needs a conversion:
var c = number(b)
fmt.Println(a, b, c)
}
输出:
{5} {5} {5}
练习 10.1 vcard.go:
定义结构体 Address
和 VCard
,后者包含一个人的名字、地址编号、出生日期和图像,试着选择正确的数据类型。构建一个自己的 vcard
并打印它的内容。
提示:
VCard 必须包含住址,它应该以值类型还是以指针类型放在 VCard 中呢?
第二种会好点,因为它占用内存少。包含一个名字和两个指向地址的指针的 Address 结构体可以使用 %v 打印:
{Kersschot 0x126d2b80 0x126d2be0}
练习 10.2 personex1.go:
修改 personex1.go,使它的参数 upPerson
不是一个指针,解释下二者的区别。
练习 10.3 point.go:
使用坐标 X
、Y
定义一个二维 Point
结构体。同样地,对一个三维点使用它的极坐标定义一个 Polar
结构体。实现一个 Abs()
方法来计算一个 Point
表示的向量的长度,实现一个 Scale()
方法,它将点的坐标乘以一个尺度因子(提示:使用 math
包里的 Sqrt()
函数)(function Scale that multiplies the coordinates of a point with a scale factor)。
练习 10.4 rectangle.go:
定义一个 Rectangle
结构体,它的长和宽是 int
类型,并定义方法 Area()
和 Perimeter()
,然后进行测试。