# 10.1 结构体定义 结构体定义的一般方式如下: ```go type identifier struct { field1 type1 field2 type2 ... } ``` ` type T struct {a, b int}`也是合法的语法,它更适用于简单的结构体。 这个结构体里的成员都有名字(names),像field1,field2等,如果成员在代码从来也不会被用到,那么可以命名为*_*。 成员可以是任何类型,甚至是结构体本身(参考[10.5](10.5.md)),可以是函数或者接口(参考第11章)。可以定义结构体类型的一个变量,然后给它的成员像下面这样赋值: ```go var s T s.a = 5 s.b = 8 ``` 数组可以看作是一种结构体类型,不过它使用下标而不是命名的成员。 **使用new** 使用*new*函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:`var t *T = new(T)`,如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。 ```go var t *T t = new(T) ``` 写这条语句的惯用法是:`t := new(T)`,变量*t*是一个指向`T`的指针,此时结构体成员的值是它们所属类型的零值。 声明`var t T`也会给`t`分配内存,并零值化内存,但是这个时候t是类型T。在这两种方式中,t通常被称做类型T的一个实例(instance)或对象(Object)。 [Listing 10.1—structs_fields.go](examples/chapter_10/structs_fields.go)给出了一个非常简单的例子: ```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)来引用结构体的成员: ```go type myStruct struct { i int } var v myStruct // v has struct type var p *myStruct // p is a pointer to a struct v.i p.i ``` 初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下: ```go ms := &struct1{10, 15.5, "Chris"} // 此时ms的类型是 *struct1 ``` 或者: ```go var mt struct1 ms := struct1{10, 15.5, "Chris"} ``` 混合字面量语法(composite literal syntax) ` &struct1{a, b, c}` 是一种简写,底层仍然会调用` new ()`,这里值的顺序必须按照成员顺序来写。在下面的例子中能看到可以通过在值的前面放上成员名来初始化值。表达式` new(Type)` 和` &Type{}`是等价的。 结构体的典型例子是一个时间间隔(开始和结束时间以秒为单位): ```go type Interval struct { start int end int } ``` 初始化方式: ```go intr := Interval(0, 3) (A) intr := Interval(end:5, start:1) (B) intr := Interval(end:5) (C) ``` 在(A)中值必须以成员在结构体定义时的顺序给出,*&*不是必须的。(B)显示了另一种方式,成员名加上一个冒号在值的前面,这种情况下值的顺序不必一致,并且某些成员还可以被忽略掉,就像(C)那样。 结构体类型和成员的命名遵循可见性规则([4.2](4.2.md)),一个导出的结构体类型中有些成员是导出的,另一些不是,这是可能的。 下图说明了结构体类型实例和一个指向它的指针的内存布局: ```go type Point struct { x, y int } ``` 使用new初始化: ![](images/10.1_fig10.1-1.jpg?raw=true) 作为结构体字面量初始化: ![](images/10.1_fig10.1-2.jpg?raw=true) 类型strcut1在定义它的包pack1中必须是唯一的,它的完全类型名是:`pack1.struct1`。 下面的例子[Listing 10.2—person.go](examples/person.go)显示了一个结构体Person,一个方法,方法有一个类型为*Person的参数(因此对象本身是可以被改变的),以及三种不同的调用这个方法的方式: ```go 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) } ``` 第2种情况 输出: 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"`这样,可以直接通过指针,像`pers2.lastName="Woodward"`这样给结构体成员赋值,没有像C++那样需要使用`->`操作符,Go会自动做这样的转换。 注意也可以通过解指针的方式来设置值: ```go (*pers2).lastName = "Woodward" ``` **结构体和内存布局** Go语言中的结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这带来了很大的性能优势。不像Java中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,Go中的指针和这个很像。下面的例子清晰的说明了这些情况: ```go type Rect1 struct {Min, Max Point } type Rect2 struct {Min, Max *Point } ``` ![](images/10.1_fig10.2.jpg?raw=true) **递归结构体** 结构体类型可以通过自身来定义。在结构体变量是链表或二叉树的元素(通常叫节点)时,这种方法会特别有用,此时节点包含指向临近节点的链接(地址)。如下所示,链表中的`su`,树中的`ri`h和`le`分别是指向另一个节点变量的指针。 链表: ![](images/10.1_fig10.3.jpg?raw=true) 这块的data域用于存放有些数据(比如float64),su指针指向后继节点。 Go代码: ```go type Node struct { data float64 su *Node } ``` 链表中的第一个元素叫`head`,它指向第二个元素;最后一个元素叫`tail`,它没有后继元素,所以它的su为nil值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。 同样地可以定义一个双向链表,它有一个前趋节点pr和一个后继节点su: ```go type Node struct { pr *Node data float64 su *su } ``` 二叉树: ![](images/10.1_fig10.4.jpg?raw=true) 二叉树中每个节点最多能链接至两个节点:左节点(le)和右节点(ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点(`root`),底层没有子节点的节点叫叶子节点(`leaves`),叶子节点的le和ri指针为空值。在Go中可以如下定义树: ```go type Tree strcut { le *Tree data float64 ri *Tree } ``` **结构体转换** Go中的类型转换遵循严格的规则。当为结构体定义了一个alias类型时,此结构体类型和它的alias类型都有相同的底层类型,它们可以如[Listing 10.3]那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误: ```go 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* 修改*persionext1.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.3* rectangle.go:定义一个Rectangle结构体,它的长和宽是int类型,并定义方法Area()和Primeter()并测试 ## 链接 - [目录](directory.md) - 上一节:[10 结构(struct)与方法(method)](10.0.md) - 下一节:[10.2 使用工厂方法创建结构体](10.2.md)