使用结构体和指针
本章节介绍如下内容
- 结构体是什么?
- 创建结构体
- 嵌套结构体
- 自定义结构体数据结字段的默认值
- 比较结构体
- 理解共有和私有值
- 区分指针引用和值引用
结构体是由数据元素组成的结构,它是一个很有用的编程构件。
1.1 结构体是什么?
结构体是一系列具有指定数据类型的数据字段,它能够让你通过单个变量引用一系列相关的值。通过使用结构体,可在单个变量中存储众多类型不同的数据字段。存储在结构体中的值可轻松地访问和修改,这提供了一种灵活的数据结构创建方式。通过使用结构体,可提高模块化程度,还能够让你创建并传递复杂的数据结构。
还可将结构体视为用于创建数据记录(如员工记录和机票预订)的模版。
程序清单:声明并创建一个简单的结构体.go
package main
import (
"fmt"
)
// 声明结构类型
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 创建结构体
声明结构体后,就可以通过多种方式创建它。假设你已经声明了一个结构体,那么就可直接声明这种类型的变量。
type Movie struct{
Name string
Rating float32
}
var m Movie
注意:创建一个结构体实例后,各个数据字段的值为相应类型的零值。如果想要调试或查看结构体的零值,可使用 fmt 包将结构体的字段名和值打印出来。
fmt.Printf("%+v\n", m) //打印结构体
以这种方式创建结构体实例后,可使用点表示法给其字段赋值:
var m Movie
m.Name = "Metropolis"
m.Rating = 0.99
结构体数据字段是可变的,这意味着可动态地修改它们。例如,你可以修改电影的名称。然而,一旦结构体被声明或者实例被创建,就不能修改其字段的数据类型了,否则会引发编译错误。
程序清单:声明并创建一个结构体并将其赋给一个变量,然后再给这个结构体的数据字段赋值.go
package main
import "fmt"
type Movie struct {
Name string
Rating float32
}
func main() {
var m Movie
fmt.Printf("%+v\n", m)
m.Name = "Metropolis"
m.Rating = 0.9918
fmt.Printf("%+v\n", m)
}
- 关键字 var 声明变量 m。
- 没有给字段赋值,所以他们默认为零值。对于字符串,零值为空字符串"",对于 float32,零值为 0。
- 将字段的值打印到终端。
- 使用点表示法给结构体的数据字段赋值。
- 再次将结构体打印,以证明数据发字段的值发生了变化。
也可使用关键字 new 来创建结构体实例。
m := new(Movie) m.Name = "Metropolis" m.Rating = 0.99
程序清单:使用关键字 new 创建结构体实例.go
package main import ( "fmt" ) type Movie struct { Name string Rating float32 } func main() { m := new(Movie) m.Name = "Metropolis" m.Rating = 0.99 fmt.Printf("%+v\n", m) }
还可使用简短变量赋值来创建结构体实例,此时可省略关键 new。创建结构体实例时,可同时给字段赋值,方法是使用字段名、冒号和字段值。
c := Movie{Name: "Citizen Kane", Rating: 10}
也可省略字段名,按字段声明顺序来给它们赋值,但出于可维护性考虑,不推荐这么做。
c := Movie{"Citizen Kane", 10}
字段有很多时,让每个字段独占一行能够提高代码的可维护性和可读性。请注意,每行必须以逗号结尾。
c := Movie{ Name: "Citizen Kane", Rating: 10, }
使用简短变量赋值是最常用的结构体创建方式,也是推荐的方式。
程序清单:使用简短变量赋值创建结构体实例.go
package main import "fmt" type Movie struct { Name string Rating float32 } func main() { m := Movie{ Name: "Metropolis", Rating: 0.99, } fmt.Printf("%+v\n", m) }
1.3 嵌套结构体
有时候,数据结构需要包含多个层级。此时,虽然可选择使用诸如切片等数据类型,但有时候需要的数据结构更复杂。为建立复杂的数据解雇,在一个结构体中嵌套另一个结构体的方式很有用。一个这样的例子是超级英雄列表:对于每个超级英雄,都需要存储其住址,而住址本身也是一个数据结构,非常适合使用结构体表示。
type Superhero struct{
Name string
Age int
Address Address
}
type Address struct{
Name int
street string
City string
}
创建结构体 Superhero 的实例时,其中将包含一个数据字段为默认值的 Address 结构体。这可改善代码的灵活性和模块性,因为结构体 Address 也可用于其他地方。
程序清单:使用简短变量赋值创建嵌套结构体实例
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.Printf("%+v", e) // %+v 打印结构体时,会添加字段名; %v 打印相应值的默认格式 }
要访问内嵌结构体的数据字段,可使用点表示法,这意味着使用结构体变量名、圆点、数据字段名、圆点和内嵌数据字段名,如下所示:
fmt.Println(e.Address.Street)
1.4 自定义结构体数据字段的默认值
创建数据结构时,自定义数据字段的默认值是很有必要的。默认情况下,Go 给数据字段指定相应数据类型的零值。
类型 | 零值 |
布尔型(Boolean) | false |
整型(Integer) | 0 |
浮点型(Float) | 0.0 |
字符串(String) | "" |
指针(Pointer) | nil |
函数(Function) | nil |
接口(Interface) | nil |
切片(Slice) | nil |
通道(Channel) | nil |
映射(Map) | nil |
创建结构体时,如果没有给其数据字段指定值,它们将为 Go 语言中对应类型的零值。Go 语言没有提供自定义默认值的内置方法,但可使用构造函数来实现这个目标。构造函数创建结构体,并将没有指定值的数据字段设置为默认值。
type Alarm struct{ Time string Sound string } func NewAlarm(time string) Alarm{ a := Alarm{ Time: time, Sound: "Klaxon" } return a }
这里不直接创建结构体 Alarm,而是使用函数 NewAlarm 来创建,从而让字段 Sound 包含自定义的默认值。请注意,这只是一种技巧,而并非 Go 语言规范的组成部分。如果你直接创建结构体 Alarm 的实例,且没有给 Sound 赋值,它将包含默认值 ""。
通过使用构造函数来创建这种结构体实例时,字段 Sound 的默认值将为 Klaxon。请注意,可轻松地修改字段 Sound 的值,因此这种方法创建的是初始默认值,而不是常量值。
package main import ( "fmt" ) type Alarm struct{ Time string Sound: bool } func NewAlarm(time string) Alarm { a := Alarm{ Time: time, Sound: "Klaxon", } return a } func main() { fmt.Printf("%+v\n", NewAlarm("07:00")) }
1.5 比较结构体
对结构体进行比较,要先看它们的类型是否相同。对于类型相同的结构体,可使用相等性运算符来比较。要判断两个结构体是否相等,可使用 ==;要判断它们是否不等,可使用 != 。
package main import "fmt" type Drink struct { Name string Ice bool } func main() { a := Drink{ Name: "Lemonade", Ice: true, } b := Drink{ Name: "Lemonade", Ice: true, } if a == b { fmt.Println("a and b are the same") } fmt.Printf("%+v\n", a) fmt.Printf("%+v\n", a) }
package main import "fmt" type Drink struct { Name string Ice bool } func main() { a := Drink{ Name: "Lemonad", Ice: true, } b := Drink{ Name: "Lemonade", Ice: true, } if a == b { fmt.Println("a and b are the same") } fmt.Printf("%+v\n", a) fmt.Printf("%+v\n", a) }
不能对两个类型不同的结构体进行比较,否则将导致编译错误。因此,试图比较两个结构体之前,必须确定它们的类型相同。要检查结构体的类型,可使用 Go 语言包 reflect。
程序清单:使用 reflect 包检查结构体的类型
package main import ( "fmt" "reflect" ) type Drink struct { Name string Ice bool } func main() { a := Drink{ Name: "Lemonade", Ice: true, } b := Drink{ Name: "Lemonade", Ice: true, } fmt.Println(reflect.TypeOf(a)) fmt.Println(reflect.TypeOf(b)) }
1.6 理解公有和私有值
如果一个值被导出,可在函数、方法或包外面使用,那么它就是公有的;如果一个值只能在其所属上下文中使用,那么它就是私有的。
根据 Go 语言约定,结构体及其数据字段都可能被导出,也可能不导出。如果一个标识符的首字母是大写的,那么它将被导出;否则不会导出。
要导出结构体及其字段,结构体及其字段的名称都必须以大写字母开头。
1.7 区分指针引用和值引用
使用结构体时,明确指针引用和值引用的区别很重要。
数据值存储在计算机内存中红。指针包含值的内存地址,这意味着使用指针可读写存储的值。创建结构体实例时,给数据字段分配内存并给它们指定默认值;然后返回指向内存的指针,并将其赋给一个变量。使用简短变量赋值时,将分配内存并指定默认值。
a := Drink{}
赋值结构体时,明确内存方面的差别很重要。将指向结构体的变量赋值给另一个变量时,被称为赋值。
a := b
赋值后,a 与 b 相同,但它是 b 的副本,而不是指向 b 的引用。修改 b 不会影响 a,反之亦然。
package main import ( "fmt" ) type Drink struct { Name string Ice bool } func main() { a := Drink{ Name: "Lemonade", Ice: true, } b := a b.Ice = false fmt.Printf("%+v\n", b) fmt.Printf("%+v\n", a) fmt.Printf("%p\n", &a) fmt.Printf("%p\n", &b) }
- 声明结构体类型 Drink。
- 创建结构体 Drink 的一个实例,并将其赋给变量 a。
- 修改 b 的数据字段 Ice。
- 将 b 的值打印到终端。
- 将 a 的值打印到终端,以证明修改 b 不会影响 a。
- 使用 fmt.Printf 将 a 和 b 的内存地址打印到终端,以证明它们的内存地址不同。
要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用。因此使用它操作的不是结构体的副本而是其本身。要获得指针,可在变量前加上和号。、
程序清单:以指针引用的方式赋值结构体
package main import ( "fmt" ) type Drink struct { Name string Ice bool } func main() { a := Drink{ Name: "Lemonade", Ice: true, } b := &a b.Ice = false fmt.Printf("%+v\n", b) fmt.Printf("%+v\n", a) fmt.Printf("%p\n", b) fmt.Printf("%p\n", &a) }
- 将指向 a 的指针(而不是 a 本身)赋给 b,这是使用 & 字符表示的。
- 修改 b 时,将修改分配给 a 的内存,因为 a 和 b 指向相同的内存。
- 打印 a 和 b 的值时,将发现它们的值相同。请注意,由于 b 是指针,因此必须使用星号符对其进行引用。
- 将 b 和 a 的内存地址输出,以证明它们相同。
指针和值的差别很微妙,但选择使用指针还是值很容易区分;如果需要修改原始结构体实例,就使用指针;如果要操作一个结构体,但不想修改。