
3.5 结构体
映射用来存储某些数据很方便,不过也有一些局限性。比如它没有定义API,我们无法约束其仅设置特定的键,并且映射中的所有值都必须是同一个类型。因此映射并不是用来在函数之间传递数据的最佳方案。如果需求是组合这种数据,结构体是更好的选择。
如果你有面向对象的语言编程经验,可能会想知道类和结构体之间的区别。区别很简单:Go中没有类,因为它不支持继承。这并不是说Go并不具备面向对象语言的特性,我们会在第7章展开讨论。
大多数语言都有结构体的概念,因此在Go中读写结构体的语法你应该很熟悉:

结构体类型的定义包括关键字type
、结构体类型的名称、关键字struct
和一对大括号({}
)。在大括号内,我们可以列出结构体所需的字段。就像我们使用var
声明时把变量名放在前面,把变量类型放在后面一样,我们把结构体字段名放在前面,把结构体字段类型放在后面。还要注意,与映射字面量意义不同,结构体声明中的字段之间没有逗号。你可以在函数内部或外部定义一个结构体类型,但是在一个函数中定义的结构体类型只能在该函数中使用(详见第5章)。
从技术上讲,结构体可以定义在任意块级结构中,我们会在第4章讨论代码块。
定义了新的结构体类型后,我们可以对变量使用这个类型:

这里使用了var
声明方式。因为fred
没有赋值,所以它现在的值是person
结构体类型的零值,即结构体中的每一个字段都设置为零值。
结构体字面量也可以被赋值给变量:

与映射不同的是,赋值为空结构体和不赋值是一样的。这两种情况都会将结构体的所有字段设置为零值。有两种方式定义非空结构体字面量。结构体字面量可以在括号中定义以逗号分隔的字段值:

如果使用这种表示方式,则需要对结构体中的每个字段都赋值,并且赋值的顺序需要与声明时字段的顺序保持一致。
第二种结构体字面量和映射字面量相似:

你可以使用结构体中的字段名来指定对应的值。使用这种方式可以只指定需要的字段和值,并且与顺序无关。没有设置值的字段都设置为零值。以上两种定义方式不能混用:要么使用字段-键的形式,要么完全不用。对于小型结构体,第一种表示方式就可以了。其他情况下优先使用键名的形式,这种方式看起来需要写更多代码,但可以清楚地知道什么值分配给什么字段,而无须引用结构体的定义,并且这种方式更易于维护。如果使用第一种方式,当该结构体在未来添加新的字段时,那代码可能会编译失败。
通过点标记方式访问结构体中的字段:

与使用中括号读写映射类似,我们使用点标记方式读写结构体字段。
3.5.1 匿名结构体
也可以直接为一个变量指定一个结构体而不需要定义结构体的名字。这就是匿名结构体:

在以上代码中,变量person
和pet
的类型都是匿名结构体。读写匿名结构体的方式和读写具名函数是一样的,而且匿名结构体的初始化方式也和具名结构体一样。
那么这种只会关联到单个实例上的数据类型有什么用处呢?在两种情况下匿名结构体非常有用。第一种情况是将外部数据转换为结构体或将结构体转换为外部数据(就像JSON或者协议缓冲区,详见11.3节)。这个过程也被称为序列化数据和反序列化数据。
编写测试的时候匿名结构体也经常出现。我们会在第13章编写表格驱动的测试时使用匿名结构体。
3.5.2 比较和转换结构体
判断结构体是否可以进行比较取决于结构体的字段。如果结构体中的所有字段都是可以比较的类型,那么结构体就可以比较;如果字段中有映射或者切片则不能比较(在第4章中可以看到,函数和管道类型的字段也会导致结构体不能进行比较)。
与Python和Ruby不同,在Go中并不存在能够覆盖相等性判断的函数,使得==
和!=
可以处理不可比较的结构体。但是我们可以自己创建函数来比较结构体。
就像在Go中不能比较两个不同基础类型的变量一样,Go也不允许比较两个不同类型结构体的变量。不过Go可以做到结构体间的类型转换,只需满足:两个结构体的所有字段的名称、顺序和类型相同。让我们一探究竟,比如下面的结构体:

我们可以使用类型转换将firstPerson
的实例转换为secondPerson
,但是不能直接使用==
比较firstPerson
和secondPerson
的实例,因为它们类型不同:

我们不能将firstPerson
的实例转换为thirdPerson
,因为它们的字段顺序不同:

我们不能将firstPerson
的实例转换为fourthPerson
,因为它们的字段名称并不匹配:

我们不能将firstPerson
的实例转换为fifthPerson
,因为它有一个额外的字段:

匿名结构体还有一个额外的规则:如果两个结构体需要进行比较,并且其中只要有一个是匿名结构体,如果它们的字段的名称、顺序和类型相同,就可以不经过转换地进行比较。并且,因为这两个结构的字段具有相同的名称、顺序和类型,所以也可以在具名结构体和匿名结构体之间进行相互赋值:
