Files
the-way-to-go_ZH_CN/eBook/10.1.md
2015-08-13 16:22:25 +08:00

10 KiB
Raw Blame History

10.1 结构体定义

结构体定义的一般方式如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

type T struct {a, b int}也是合法的语法,它更适用于简单的结构体。

结构体里的字段都有名字像field1field2等如果字段在代码中从来也不会被用到那么可以命名它为*_*。

结构体的字段可以是任何类型,甚至是结构体本身(参考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)。

Listing 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 mt 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初始化

作为结构体字面量初始化:

类型strcut1在定义它的包pack1中必须是唯一的它的完全类型名是pack1.struct1

下面的例子Listing 10.2—person.go显示了一个结构体Person一个方法方法有一个类型为*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,树中的rile分别是指向别的节点的指针。

链表:

这块的data字段用于存放有效数据比如float64su指针指向后继节点。

Go代码

type Node struct {
    data    float64
    su      *Node
}

链表中的第一个元素叫head,它指向第二个元素;最后一个元素叫tail,它没有后继元素,所以它的su为nil值。当然真实的链接会有很多数据节点并且链表可以动态增长或收缩。

同样地可以定义一个双向链表,它有一个前趋节点pr和一个后继节点su

type Node struct {
    pr      *Node
    data    float64
    su      *su
}

二叉树:

二叉树中每个节点最多能链接至两个节点:左节点(le)和右节点(ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点(root),底层没有子节点的节点叫叶子节点(leaves),叶子节点的leri指针为nil值。在Go中可以如下定义二叉树

type Tree strcut {
    le      *Tree
    data    float64
    ri      *Tree
}

结构体转换

Go中的类型转换遵循严格的规则。当为结构体定义了一个alias类型时此结构体类型和它的alias类型都有相同的底层类型它们可以如[Listing 10.3]那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误:

Listing 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 persionext1.go

修改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(),然后进行测试。

链接