Structs

在Go语言中最简单的自定义类型是基于内置类型之上的类型——比如,type Integer int 创建了一个Integer类型,然后可以给这个自定义类型添加我们的方法。自定义类型也可以是基于structs的,这样就可以在自定义类型中聚合和嵌入值。特别是当这些值——在struct的上下文中称为字段(field)——是不同的类型,因此也不可以存储在一个slice(除非是[]interface)时非常有用。Go语言中的结构体比C++更接近于C的结构体,同时因为Go struct支持嵌入,所以使用起来更加方便。

我们已经在之前的章节中看过许多struct的例子,并且依然后贯穿本书之后的章节。尽管如此,这里还是有许多struct的特性,是我们之前没有看到的,所以我们先通过一个示例来展示这些特性。

points := [][2]int{{4, 6}, {}, {-7, 11}, {15, 17}, {14, -8}}
for _, point := range points {
fmt.Printf("(%d, %d) ", point[0], point[1])
}

在这段代码里,points变量是一个slice, 它保存的是一个[2]int类型的数组,所以我们必须使用[]索个操作符获取每一个坐标。(顺便说一下,{}元素跟{0,0}一样)。这对于小量的简单数据来说没有什么问题,但这里有一个更好的方法,使用匿名struct.

points := []struct{ x, y int }{{4, 6}, {}, {-7, 11}, {15, 17}, {14, -8}}
for _, point := range points {
    fmt.Printf("(%d, %d) ", point.x, point.y)
}

这里,points变量也是slice, 但它保存的是一个struct{x, y int}. 虽然这个struct自身没有名字,我们通过它的字段名字访问它的数据,这比使用数组索引[]更简单和安全.

Struct Aggregation and Embedding

我们可以像嵌入接口那样,嵌入struct. 或者其它类型,即在一个struct中以匿名字段的形式,包含另一个类型的名字。(当然,如果我们给内部的struct一个变量的名字,它就变成一个聚合的命名字段,而不是嵌入的匿名字段.)

通常被嵌入的字段可以直接使用.(dot)选择操作符访问,而可以不需要类型名字,但如果外面struct有一个跟内部struct相同名的字段,我们就必须加上类型名字,以消除歧义。

在结构中,每一个字段都必须是唯一的。对于被嵌入字段,唯一性要求更加严格,以避免歧义。比如,如果我们有一个被嵌入的字段类型为Integer, 因此我们还可以称另外的字段为Integer2和BigInteger,因为这三个字段的名字是完全不同。但是其它的字段不可以是Matrix.Integera或者*Integer命名,因为这些名字的最后部分跟嵌入的Integer一样。

Embedding Values

让我们先看两个简单的struct的例子

type Person struct {
    Title string // Named field (aggregation)
    Forenames []string // Named field (aggregation)
    Surname string // Named field (aggregation)
}
type Author1 struct {
    Names Person // Named field (aggregation)
    Title []string // Named field (aggregation)
    YearBorn int // Named field (aggregation)
}

author1 := Author1{Person{"Mr", []string{"Robert", "Louis", "Balfour"},
    "Stevenson"}, []string{"Kidnapped", "Treasure Island"}, 1850}
fmt.Println(author1)
author1.Names.Title = ""
author1.Names.Forenames = []string{"Oscar", "Fingal", "O'Flahertie",
"Wills"}
author1.Names.Surname = "Wilde"
author1.Title = []string{"The Picture of Dorian Gray"}
author1.YearBorn += 4
fmt.Println(author1)
/*
Stevenson, Robert Louis Balfour, Mr (1850) "Kidnapped" "Treasure Island"
Wilde, Oscar Fingal O'Flahertie Wills (1854) "The Picture of Dorian Gray"
*/

我们在开始的地方创建了一个Author1的值,并按照字段的顺序为它填充了所有的字段,并打印. 然后在修改它字段的值,再次打印。

type Author2 struct {
    Person // Anonymous field (embedding)
    Title []string // Named field (aggregation)
    YearBorn int // Named field (aggregation)
}

为了嵌入一个匿名字段,我们可以直接使用我们想要嵌入类型的名字(或者接口,如之后所见),而不需要为它指名变量的名字。我们可以直接访问这些被嵌入的字段(i.e.,不需要指名type或接口的名字), 如果当我们需要区分相同字段的时,则需要使用到类型的名字或者接口的名字。

Author2 struct以嵌入一个Person struct. 这样我们就可以直接访问Person中的字段(除非我们需要消除歧义)。

author2 := Author2{Person{"Mr", []string{"Robert", "Louis", "Balfour"},
    "Stevenson"}, []string{"Kidnapped", "Treasure Island"}, 1850}
fmt.Println(author2)
author2.Title = []string{"The Picture of Dorian Gray"}
author2.Person.Title = "" // Must use the type name to disambiguate
author2.Forenames = []string{"Oscar", "Fingal", "O'Flahertie", "Wills"}
author2.Surname = "Wilde" // Same as: author2.Person.Surname = "Wilde"
author2.YearBorn += 4
fmt.Println(author2)

创建Author2的值跟Author1一样,但是我们可以直接引用Person中的字段(e.g.,author2.Forenames)——除了我们需要区分(author2.Person.Title和author2.Title).

Embedding Anonymous Values That Have Methods

哪果一个被嵌入的字段含有方法,我们直接在包含它的struct上调用这些方法,并且在调用时,仅传递这个被传递的字段,作为方法的接收者。

type Count int
func (count *Count) Increment() { *count++ }
func (count *Count) Decrement() { *count-- }
func (count Count) IsZero() bool { return count == 0 }


type Tasks struct {
    slice []string // Named field (aggregation)
    Count // Anonymous field (embedding)
}
func (tasks *Tasks) Add(task string) {
    tasks.slice = append(tasks.slice, task)
    tasks.Increment() // As if we had written: tasks.Count.Increment()
}
func (tasks *Tasks) Tally() int {
    return int(tasks.Count)
}

我们早些时定义的Count类型(原书259), 这个Task struct有两个字段,一个是聚合的字符串slice, 一个是嵌入的Count值。如Tasks.Add()方法所示,我们可以直热闹使用Count值方法。

tasks := Tasks{}
fmt.Println(tasks.IsZero(), tasks.Tally(), tasks)
tasks.Add("One")
tasks.Add("Two")
fmt.Println(tasks.IsZero(), tasks.Tally(), tasks)
/*
true 0 {[] 0}
false 2 {[One Two] 2}
*/

在这里我们创建了一个Tasks的值,并且调用了它的Tasks.Add(), Tasks.Tolly(), 和Tasks.Count.IsZero()方法。即使我们没有定义Tasks.String()方法,Go依然可以产生合理的输出。(请注意,在这里我们没有将Tally()方法,改为Count(),这是因为会与被嵌入的Tasks.Count值相冲突,导致不能被编译)。

非常重要的一点是被嵌入字段的方法,在包含这个字段的值上调用,仅传递被包含的这个字段,作为方法的接收者。所以当Tasks.IsZero(), Tasks.Increment(), or 其它Count方法在Tasks值上调用时,这些方式的接收者都是一个Count(or *Count值),而不是Tasks的值。

在这个例子中,Tasks类型有它自己的方法(Add()和Tally()), 以及被嵌入的Count类型的方法(Increment(), Decrement(),和IsZero()). 在然在Tasks类型中可以以相同的方法名字,覆盖Count类型中的方法。

Embedding Interfaces

除了可以在structs中聚合和嵌入具体类型,也可以聚合和嵌入接口.(相反的,不可在接口中聚合和嵌入struct, 因为一个接口完全是抽像类型,所以这样的聚合或者嵌入不符合语义)。当一个struct聚合或者嵌入一个接口,这意味着struct可以存储任何实现了这个接口的类型。

通过以下的例子,我们完成对struct的学习。这个例子向我们展示了如何支持可选("option")值, 这个可选择值包含一个短的和一个长的名字。(e.g., "-o" and "--outfile"),值类型为特定的类型(可选参数的值可以是int, float64, string), 并且有一些常用的方法。(这个例子的目的仅仅是用来说明,实际的使用还是请参考标准库中flag package, 或者第三方中的godashboard.appspot.com/project)

type Optioner interface {
    Name() string
    IsValid() bool
}
type OptionCommon struct {
    ShortName string "short option name"
    LongName string "long option name"
}

Optioner接口指名了方法签名,这些方法是我们可选值类型必须提供的。OptionCommon struct提供了所有option类型,共同部分的字段。Go允许我们对struct字段进行注释(从Go专业术语上称为 tags ). 这些tags没有功能上的目的,但是不同于注释——它们通过Go反射(reflection)进行访问(9.4.9, 原书第427)。有的程序员会使用tags指定字段验证——比如,有一个字符串字段,它的tags为"check:len(2,30)", 或者数字字段,标签为"check:range(0,500)".

type IntOption struct {
    OptionCommon // Anonymous field (embedding)
    Value, Min, Max int // Named fields (aggregation)
}
func (option IntOption) Name() string {
    return name(option.ShortName, option.LongName)
}
func (option IntOption) IsValid() bool {
    return option.Min <= option.Value && option.Value <= option.Max
}
func name(shortName, longName string) string {
    if longName == "" {
        return shortName
    }
    return longName
}

这里完成了IntOption的实现,加上未导出的name()函数。由于OptionCommon struct是被嵌入的,我们可以直接访问它的字段——如我们在IntOption.Name()方法中所做的那样。IntOption实现了Optioner接口(因为它提供了Name()和IsValid()方法)。

虽然name()函数非常简单,但我们还是选择一个单独的函数,而不是在IntOption.Name()方法中实现。这使得IntOption.Name()方法变得更多,同时我们将这个函数name()用于其它的自定义Options.比如,GenericOption.Name()和StringOption.Name()方法。这在Go语言中是一种常用模式,我们会在之后的章节中在次看到。

StringOptions的实现跟IntOption非常类型,所以我们不在这里展示(不同的地方是,Value字段的类型为string, IsValid()返回true, 条件是Value为非空字符串)。对于FloatOption,我们使用嵌入一个Optioner接口,而不是OptionCommon。

type FloatOption struct {
    Optioner // Anonymous field (interface embedding: needs concrete type)
    Value float64 // Named field (aggregation)
}

这里完成了FloatOption。被嵌入的是Optioner字段,这表明当我们创建FloatOption类型的值时,我们必须对嵌入字段(Optioner的位置)赋于一个实现了Optioner接口的值。

type GenericOption struct {
    OptionCommon // Anonymous field (embedding)
}
func (option GenericOption) Name() string {
    return name(option.ShortName, option.LongName)
}
func (option GenericOption) IsValid() bool {
    return true
}

这里实现了GenericOption, 它满足了Optioner接口。

FloatOption类型嵌入了一个Optioner接口的字段,所在在创建FloatOption时的第一个位置(Optioner字段的位置)并必赋于一个实现了Optioner接口的具体类型。在这里我们可以将GenericOption的值,赋值给FloatOptioner字段。

接下来让我们看看如何创建和使用它们。

fileOption := StringOption{OptionCommon{"f", "file"}, "index.html"}
topOption := IntOption{
    OptionCommon: OptionCommon{"t", "top"},
    Max: 100,
}
sizeOption := FloatOption{
    GenericOption{OptionCommon{"s", "size"}}, 19.5}
for _, option := range []Optioner{topOption, fileOption, sizeOption} {
    fmt.Print("name=", option.Name(), " • valid=", option.IsValid())
    fmt.Print(" • value=")
    switch option := option.(type) { // shadow variable
    case IntOption:
        fmt.Print(option.Value, " • min=", option.Min,
            " • max=", option.Max, "\n")
    case StringOption:
        fmt.Println(option.Value)
    case FloatOption:
        fmt.Println(option.Value)
    }
}
/*
name=top • valid=true • value=0 • min=0 • max=100
name=file • valid=true • value=index.html
name=size • valid=true • value=19.5
*/

fileOption是一个StringOption类型的值,每一个字段按照次序赋于相应的值。对于topOption,它是IntOption类型,我们仅对它赋值了OptionCommon和Max字段,其它的字段初始化为零值(i.e.,Value和Min字段). Go 允许我们在创建struct,可以通过fieldName: fieldValue的语法初始我们想要的字段。当这种语法被使用时,任何没有被明确指名的字段,会自动设置为它们的零值。

sizeOption是一个FloatOption类型,它的第一个字段是Optioner接口,所以我们必须应用一个实现了这个接口的具体类型。为此我们创建了一个GenericOption的值。