[Go 入门] 第六章 使用结构体和指针

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

结构体是由数据元素组成的构件,是一个很有用的编程构件, 本文内容:

结构体是什么

结构体是一系列具有指定数据类型的数据字段,它能够通过单个变量引用一系列相关的值。通过使用结构体,可在单个变量中储存众多类型不同的数据字段。储存在结构体中的值可轻松地访问和修改,这提供了一种灵活的数据结构创建方式。通过使用结构体,可提高模块化程序,还能创建并传递复杂的结构数据

1
2
3
4
5
6
7
8
9
10
11
12
13
type Movie struct {
Name string
Rating float32
}

func main() {
m := Movie{
Name: "Citizen Kane",
Rating: 10,
}
fmt.Println(m.Name, m.Rating)
}

对上面代码的解读为:

  • 关键字 type 指定一种新类型
  • 将新类型的名称指定为 Movie
  • 类型名右边是数据类型,这里为结构体
  • 在大括号内,使用名称和类型指定了一系列数据字段。请注意,此时没有给数据字段复制。可将结构体视为模板
  • 在 main 函数中,使用简短变量赋值声明并初始化了变量m,给数据字段指定的值为相应的数据类型
  • 使用点表示法访问数据字段并将其打印到控制台

创建结构体

声明结构体后,就可以通过多种方式创建,如已声明一个结构体,那么就可直接声明这种类型的变量

1
2
3
4
5
6
type Movie struct {
Name string
Rating float32
}

var m Movie

这样就创建好了结构体,并将各个数据字段设置为相应数据类型的零值。要调试和查看结构体的值,可以使用占位符 %+v

这将把字段名和值打印到在终端。创建结构体时,如没有初始化,则 Go 将把每个数据字段设置为相应数据类型的零值

{Name: Rating:0}

以这种方式创建的结构体,还可以用点表示法进行字段赋值

1
2
3
var m Movie
m.Name = "buck"
m.Rating = 0.99

结构体数据字段的值时可变的,这意味着可动态地修改它们。一旦结构体被声明或者实例被创建,就不能再修改其字段的数据类型了。

也可以使用关键字new来创建结构体实例。关键字new创建结构体 Movie 的一个实例(为其分配内存);将这个结构体实例赋给变量 m 后,就可以像前面那样使用点表示法给数据字段赋值了

1
2
3
m := new(Movie)
m.Name = "cook"
m.Rating = 0.99

还可以通过简短变量赋值来创建结构体实例,此时可省略关键字new。创建结构体时,可同时给字段赋值,方式是使用字段名、冒号和字段值

c := Movie{Name: "Ela", Rating: 0.8}

也可忽略字段名,按字段声明顺序给它赋值,但出于可维护性考虑,不推荐这样做

c := Movie{"Ela", 0.8}

字段很多时,让每个字段独占一行能够提高代码的可维护性和可读性,如果这样做,需要在最后一个数据字段所在行也以逗号结尾

1
2
3
4
c := Movie {
Name: "Ela",
Rating: 0.8,
}

使用简短变量赋值是最常用的结构体创建方式,也是推荐的方式

提示:

类C语言也支持结构体,结构体并非创建面向对象代码的方式,而是一种数据结构创建方式,旨在满足数据建模需求

嵌套结构体

有时候,数据结构需要包含多个层级,就可以创建一个结构体嵌套另一个结构体

1
2
3
4
5
6
7
8
9
10
11
type Superhero struct {
Name string
Age int
Address Address
}

type Address struct {
Number int
Street string
city string
}

创建结构体 Superhero 时,其中将包含一个数据字段为默认值的 Address 结构体,这可改善代码的灵活性和模块性,因结构体 Address 也可用于其他地方

下面将会用简短变量赋值创建一个嵌套结构体实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type Superhero struct {
Name string
Age int
Address Address
}

type Address struct {
Number int
Street string
City string
}

func main() {
e := Superhero{
Name: "Batman",
Age: 32,
Address: Address{
Number: 1007,
Street: "Mountain Drive",
City: "Gotham",
},
}
fmt.Println(e)
}

自定义结构体数据字段的默认值

Go 语言中的零值:

类型 零值
布尔型 (Boolean) false
整型 (Integer) 0
浮点型 (Float) 0.0
字符串 (String) “ “
指针 (Pointer) nil
函数 (Function) nil
接口 (Interface) nil
切片 (Slice) nil
通道 (Channel) nil
映射 (Map) nil

创建结构体时,如果没有给出默认的值,则根据数据类型为上述的值

Go 语言没有提供自定义默认值的内置方法,但可以通过构造函数来实现

1
2
3
4
5
6
7
8
9
10
11
12
type Alarm struct {
Time string
Sound string
}

func NewAlarm(time string) Alarm {
a := Alarm{
Time: time,
Sound: "Klaxon",
}
return a
}

代码中,不直接创建结构体 Alarm,而是使用函数 NewAlarm 来创建,从而让字段 Sound 包含自定义的默认值。这是一种技巧,而并非 Go 语言规范的组成部分

比较结构体

对结构体进行比较,要看它们的类型和值是否相同,对于类型相同的结构体,可使用相等性运算符来比较,要判断两个结构体是否相等,可以使用==,要判断它们不等可用!=,下面代码中,创建了两个数据字段值相等的结构体,由于它们相等,所以返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Drink struct {
Name string
Ice bool
}

func main() {
a := Drink{
Name: "buck",
Ice: true,
}
b := Drink{
Name: "buck",
Ice: true,
}
if a == b {
fmt.Println("a and b are the same")
}
}

不能对两个类型不同的结构体进行比较, 否则将导致编译错误。因此,试图比较两个结构体之前,必须先确定它们的类型相同。要检查结构体的类型,可使用 Go 语言包的 reflect

1
2
3
4
5
6
7
8
9
10
11
12
type Drink struct {
Name string
Ice bool
}

func main() {
a := Drink{
Name: "buck",
Ice: true,
}
fmt.Println(reflect.TypeOf(a))
}

Go 语言规定,结构体及其数据字段都可能被导出,也可能不导出。如果一个标识符的首字母是大写,那么可被导出,否则将不会导出

区分指针引用和值引用

使用结构体时,明确指针引用和值引用的区别很重要。之前有说过,数据值储存在计算机内存中。指针包含值的内存地址,这意味着,指针可以读写储存的值。创建结构体时,给数据字段分配内存并给他们指定默认值;然后返回指向内存的指针,并将其赋给一个变量,使用简短变量赋值时,将分配内存并指定默认值

a := Drink{}

复制结构体时,明确内存方面的差别很重要。将指向结构体的变量赋给另一个变量时,被称为赋值

a := b

赋值后,虽然 a 与 b 相同,但它是 b 的副本,而不是指向 b 的引用。修改 b 不会影响 a,反之亦然

值引用复制结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
type Drink struct {
Name string
Ice bool
}

func main() {
a := Drink{
Name: "buck",
Ice: true,
}
b := a
b.Ice = false
}

解读如下:

  • 声明结构体类型 Drink
  • 创建结构体 Drink 的一个实例,并将其赋给变量a
  • 声明变量b并将a赋给它
  • 修改b的数据字段Ice

指针引用复制结构体

要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身。要获得指针,可在变量名前加上和号

1
2
3
4
5
6
7
8
9
10
11
12
13
type Drink struct {
Name string
Ice bool
}

func main() {
a := Drink{
Name: "buck",
Ice: true,
}
b := &a
b.Ice = false
}

解读如下:

  • 将指向 a 的指针 (而不是 a 本身)赋给b,这是使用和号字符表示的
  • 修改 b 时,将修改分配给 a 的内存,因为a和b指向相同的内存
  • 打印 a 和 b 的值时,将发现它们的值相同。请注意,由于b是指针,因此必须使用星号字符对其进行引用
  • 将b和a的内存地址打印到控制台,以证明它们相同

问题列表

  • 本书前面介绍了3种创建结构体的方式,我该使用哪种呢?

    推荐使用简短变量赋值方式(:=)来创建结构体。关键字new和变量声明的方式也合法,但不那么常用

  • 我明白了如何嵌套结构体,请问最多可以嵌套多少层呢?

    对结构体嵌套层级数没有任何限制,但如果嵌套层级太多,可能昭示着使用其他数据结构是更好的选择

  • 结构体数据字段可以是任何数据类型吗?

    是的,在结构体中可以使用任何数据类型,包括自定义的类型