本章节介绍结构体,通过本章节,你将了解到如何初始化结构体、结构体的零值、怎样将自定义的默认值分配给结构体的数据字段、引用结构体的值,还会介绍到值引用和指针引用的区别。
7.1 什么是结构体
结构体是声明了数据类型的数据字段的集合,这些数据类型可以相同或不同,通过结构体的变量名可以访问一系列分组值。结构体丰富了编程过程中创建数据结构的方法,是很常用的基础知识之一。
我们来尝试用结构体记录下库里某场 NBA 比赛的个人数据:
package main
import "fmt"
type PersonalData struct {
Name string
Points int
Assists int
Rebounds int
}
func main() {
var Pd PersonalData
Pd.Name = "Curry"
Pd.Points = 51
Pd.Assists = 13
Pd.Rebounds = 5
fmt.Printf("%+v\n", Pd)
}
代码释义:
var 关键字声明变量 Pd,类型是 PersonalData 结构体类型;
使用
.
表示法,为结构体中的字段赋值;打印结构体的值到终端;
使用 type 关键字自定义结构体类型 PersonalData。在定义时并未分配值,所以会被分配零值。string 类型,零值为””,int 类型零值为0。
也可以使用 new 关键字创建结构体实例:
package main
import "fmt"
type PersonalData struct {
Name string
Points int
Assists int
Rebounds int
}
func main() {
Pd := new(PersonalData)
Pd.Name = "Curry"
Pd.Points = 51
Pd.Assists = 13
Pd.Rebounds = 5
fmt.Printf("%+v\n", Pd)
}
当然,还可以使用短变量赋值来创建结构体实例:
package main
import "fmt"
type PersonalData struct {
Name string
Points int
Assists int
Rebounds int
}
func main() {
Pd := PersonalData {
Name: "Curry",
Points: 51,
Assists: 13,
Rebounds: 5,
}
fmt.Printf("%+v\n", Pd)
}
使用字段名称创建实例时,可以将值分配给结构体中的字段,之间用冒号分割,没有被赋值的字段,则被赋予零值。也可以省略字段名,但要按照声明时的顺序赋值,出于可维护性考虑,不建议在赋值时省略字段名。
随着字段列表的增加,为了提高可维护性和可读性,每个字段和值可以独立成行,且以逗号结尾。使用短变量赋值创建结构体实例是最常见的方式,也是推荐的方式。
7.2 匿名结构体
在定义结构体时不指定结构体的名称,就是匿名结构体。匿名结构体只能在函数内部定义,用于临时性功能。在定义时,直接给匿名结构体初始化一个变量:
var user struct {
id int
name string
}
user.id = 30
user.name = "Curry"
// or
user := struct {
id int
name string
}{30, "Curry"}
7.3 内嵌结构体
内嵌结构体又叫结构体的命名组合。有时,数据结构有多层嵌套,比较复杂。这时将一个结构体嵌套在另一个结构体中,可能是对复杂结构建模的有效方法。下面是一个球员列表的示例,需要为每位球员存储一个地址。地址可以作为独立的数据结构,并且能很好的映射到球员列表的结构体中:
package main
import "fmt"
type Address struct {
Number int
Street string
City string
}
type PlayerList struct {
Name string
PlayerNumber int
Address Address
}
func main() {
pl := PlayerList{
Name: "Curry",
PlayerNumber: 30,
Address: Address{
Number: 18,
Street: "SAN mateo avenue",
City: "San Francisco",
},
}
fmt.Printf("%+v\n", pl)
}
访问嵌套结构体中的数据,可以使用 . 表示法,通过结构体的变量名称,后跟.,然后是数据元素名,再跟一个.,最后是嵌套数据的元素名称:
fmt.Println(pl.Address.City)
7.4 匿名内嵌结构体
嵌入结构体时,可以省略属性名,类似面向对象中的继承。Go 会以隐藏的方式给它分配一个和类型相同的名字。
package main
import "fmt"
type Address struct {
Number int
Street string
City string
}
type PlayerList struct {
Name string
PlayerNumber int
Address
}
func main() {
pl := PlayerList{
Name: "Curry",
PlayerNumber: 30,
Address: Address{
Number: 18,
Street: "SAN mateo avenue",
City: "San Francisco",
},
}
fmt.Printf("%+v\n", pl)
fmt.Println(pl.City)
fmt.Println(pl.Address.City)
}
命名嵌入和匿名嵌入的区别:
命名嵌入:如果访问组合(嵌入的对象)必须使用对象名访问;
匿名嵌入:可以通过结构体的实例名直接访问。访问对象时,如果本地属性不存在,则在嵌入的匿名对象中查找;
如果多个结构体中都包含相同的属性名,访问匿名嵌入的结构体属性时必须通过完整的访问关系进行访问。
7.5 自定义结构体默认值
在初始化结构体时未对元素赋值,那么它们的默认值是对应类型的零值。尽管 Go 没有提供原生的创建结构体时分配自定义默认值的方法,但我们可以通过构造函数来实现。例如,PlayerList 结构体,用来记录 NBA 球员的信息,这时可以将 League 字段的值默认设置为 NBA,我们只需要填写球员名字和球衣号码即可:
package main
import "fmt"
type PlayerList struct {
League string
Name string
PlayerNumber int
}
func NewPlayerList(name string, number int) PlayerList {
pl := PlayerList{
League: "NBA",
Name: name,
PlayerNumber: number,
}
return pl
}
func main() {
fmt.Printf("%v\n", NewPlayerList("Curry", 30))
}
7.6 结构体比较
对结构体进行比较,可以判断它们是否为同一类型,是否有相同值。==和!=用来判断两边的数据是否相等或不等:
package main
import "fmt"
type NBA struct {
Name string
UniformNumber int
}
func main() {
Curry := NBA {
Name: "Curry",
UniformNumber: 30,
}
Durant := NBA {
Name: "Kevin",
UniformNumber: 15,
}
if curry == Durant {
fmt.Println("The Same.")
} else {
fmt.Printf("%v\n", Curry)
fmt.Printf("%v\n", Durant)
}
}
不同类型的结构体进行比较会引发编译错误,因此,在做对比前需要检查结构体是否为同一类型。reflect 包提供了这个功能:
package main
import (
"fmt"
"reflect"
)
type NBA struct {
Name string
UniformNumber int
}
func main() {
Curry := NBA{
Name: "Curry",
UniformNumber: 35,
}
Durant := NBA{
Name: "Kevin",
UniformNumber: 15,
}
fmt.Println(reflect.TypeOf(Curry))
fmt.Println(reflect.TypeOf(Durant))
}
7.7 公有值和私有值
如果值导出到函数、方法或包外依然可用,则为公有值,否则为私有值。私有值仅在其上下文中可用。结构体和结构体中的元素,都可以根据 Go 约定导出或不导出。以大写字母开头的是可以导出的,其他则不能。为了导出结构体和其元素,结构名和元素属性名必须以大写字母开头。
7.8 值引用和指针引用
使用结构体时,熟悉值引用和指针引用很有必要。在第三章变量章节,介绍了数据的值存储在内存中。指针保存了一个值的存储地址,可以通过引用指针,读取或写入已存储的值。在初始化结构体时,将为结构体中的元素及其默认值分配内存。使用短变量声明变量,并分配默认值:
Curry := NBA{}
如果复制 Curry 结构体,在内存上有一个重要的区别需要理解:将引用该结构体的变量分配给另一个值时,称为值分配:
Durant := Curry
此时 Durant 与 Curry 相同,是 Curry 的副本,而不是对 Curry 的引用。对 Durant 的任何更改,都不会体现在 Curry 上,反之亦然。
package main
import "fmt"
type NBA struct {
Name string
Team string
}
func main() {
Curry := NBA{
Name: "Curry",
Team: "Warriors",
}
Durant := Curry
fmt.Printf("%v\n", Curry)
fmt.Printf("%v\n", Durant)
fmt.Printf("%v\n", &Curry)
fmt.Printf("%v\n", &Durant)
}
代码释义:
定义一个名称为 NBA 的结构体
定义一个 Curry 变量,并赋值为 NBA
定义一个 Durant 变量,并赋值为 Curry
打印 Curry 和 Durant 的值
打印 Curry 和 Durant 的内存地址
如果要修改原始结构中包含的值,应该使用指针。指针是对内存地址的引用,不会对结构体的副本进行操作,而是会更新引用基础内存的所有变量。下面是更新指针值的示例:
package main
import "fmt"
type NBA struct {
Name string
UniformNumber int
}
func main() {
Curry := NBA{
Name: "Curry",
UniformNumber: 35,
}
Durant := &Curry
Durant.UniformNumber = 15
fmt.Printf("%v\n", Curry)
fmt.Printf("%v\n", *Durant)
fmt.Printf("%v\n", &Curry)
fmt.Printf("%v\n", Durant)
}
变动部分的代码释义:
Durant 不再是通过值引用,而是通过指针引用
当 Durant 更新时,分配给 Curry 的底层内存也更新了。由于 Curry 和 Durant 都引用了相同的底层内存,所以它们的值是相同的
打印 Durant 和 Curry 的值可以看到结果相同。需要注意的是,Durant 现在是一个指针,必须使用*取消对它的引用
打印 Durant 和 Curry 的底层内存地址,结果也是相同的
指针引用和值引用之间的区别很细微,但是选择使用指针还是值很简单。如果需要修改(或改变)结构体的初始化值,请使用指针引用。如果需要对结构体进行操作,但不希望修改结构体的初始化值时,请使用值引用。
7.9 总结
本章主要介绍结构体(Struct),包括如何创建结构体、如何使用结构体表示复杂的数据结构。在对结构体进行初始化时,如果使用顺序初始化的方式,每个成员必须进行初始化;如果使用指定成员初始化,没有初始化的成员,自动赋零值。使用构造函数在创建结构体时自定义值。了解了结构体的导出及其指针引用与值引用的区别。结构体提供了一种以编程方式进行推理的方法,是一种强大的基本结构。当你再观察身边事物的时候,可以尝试在结构体中表示它们,比如一副口罩、一只鸟。