在前面的章节中,您可能已经在一些示例代码中瞥见了组合类型(如数组、切片、映射和结构)的使用。虽然早期接触这些类型可能会让您感到好奇,但请放心,在本章中,您将有机会了解所有这些复合类型。本章继续第 4 章、数据类型中开始的内容,讨论内容包括以下主题:
- 数组类型
- 切片类型
- 地图类型
- 结构类型
正如您在其他语言中会发现的那样,Go 数组是用于存储数字索引的相同类型的序列值的容器。以下代码段显示了分配给数组类型的变量示例:
var val [100]int
var days [7]string
var truth [256]bool
var histogram [5]map[string]int
golang.fyi/ch07/arrgo。
请注意,上一示例中分配给每个变量的类型是使用以下类型格式指定的:
这是同一件事,这是同一件事,这是同一件事,这是同一件事,3 乘以 3,这是同一件事,这是同一件事,这是同一件事,这是同一件事,这是同一件事,这是同一件事,这是同一件事,这是 3 乘以 3,这是同一件事,这是同一件事,这是
数组的类型定义由其长度(括在括号内)和存储元素的类型组成。例如,days
变量被分配了一个类型[7]string
。这是一个重要的区别,因为 Go 的类型系统将存储相同类型元素但长度不同的两个数组视为不同类型。以下代码说明了这种情况:
var days [7]string
var weekdays [5]string
尽管这两个变量都是带有类型为string
的元素的数组,但类型系统将days
和weekdays
变量视为不同的类型。
在本章后面,您将看到如何通过使用切片类型而不是数组来缓解这种类型限制。
数组类型可以定义为多维。这是通过组合和嵌套一维数组类型的定义来完成的,如以下代码段所示:
var board [4][2]int
var matrix [2][2][2][2] byte
golang.fyi/ch07/arrgo。
对于多维数组,Go 没有单独的类型。具有多个维度的数组由相互嵌套的一维数组组成。下一节将介绍如何初始化一维数组和多维数组。
当数组变量未显式初始化时,它的所有元素都将为声明的元素类型分配零值。可以使用具有以下常规格式的复合文字值初始化数组:
<数组 _ 类型>{<元素值逗号分隔列表>}
数组的文字值由数组类型定义(在上一节中讨论)和一组逗号分隔的值组成,这些值用花括号括起来,如以下代码段所示,其中显示了几个正在声明和初始化的数组:
var val [100]int = [100]int{44,72,12,55,64,1,4,90,13,54}
var days [7]string = [7]string{
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
}
var truth = [256]bool{true}
var histogram = [5]map[string]int {
map[string]int{"A":12,"B":1, "D":15},
map[string]int{"man":1344,"women":844, "children":577,...},
}
golang.fyi/ch07/arrinit.go
文本中的元素数必须小于或等于数组类型中声明的大小。如果定义的数组是多维数组,则可以通过将每个维度嵌套在另一个维度的括号内,使用文字值初始化该数组,如以下示例代码段所示:
var board = [4][2]int{
{33, 23},
{62, 2},
{23, 4},
{51, 88},
}
var matrix = [2][2][2][2]byte{
{{{4, 4}, {3, 5}}, {{55, 12}, {22, 4}}},
{{{2, 2}, {7, 9}}, {{43, 0}, {88, 7}}},
}
golang.fyi/ch07/arrinit.go
下面的代码段显示了可以指定数组文本的两种附加方式。在初始化过程中,可以省略数组的长度并用椭圆替换。下面将把类型[5]string
分配给变量weekdays
:
var weekdays = [...]string{
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
}
数组的文字值也可以被索引。如果只想初始化某些数组元素,而允许使用其自然零值初始化其他数组元素,则此选项非常有用。以下规定了位置 0、2
、4
、6
、8
处元素的初始值。其余元素将被分配空字符串:
var msg = [12]rune{0: 'H', 2: 'E', 4: 'L', 6: 'O', 8: '!'}
数组的类型可能会变得难以重用。对于每个声明,都需要重复声明,这可能容易出错。惯用的处理方法是使用类型声明来别名数组类型。为了说明其工作原理,以下代码段声明了一个新的命名类型matrix
,使用多维数组作为其基础类型:
type matrix [2][2][2][2]byte
func main() {
var mat1 matrix
mat1 = initMat()
fmt.Println(mat1)
}
func initMat() matrix {
return matrix{
{{{4, 4}, {3, 5}}, {{55, 12}, {22, 4}}},
{{{2, 2}, {7, 9}}, {{43, 0}, {88, 7}}},
}
}
golang.fyi/ch07/arrtype
声明的命名类型matrix
可以在使用其底层数组类型的所有上下文中使用。这允许简化语法,以促进复杂数组类型的重用。
数组是静态实体,一旦用指定的长度声明,它们的大小就不能增长或收缩。当程序需要分配预定义大小的顺序内存块时,数组是一个很好的选择。当声明数组类型的变量时,它就可以在没有任何进一步分配语义的情况下使用了。
因此,image
变量的以下声明将分配一个由 256 个相邻的int
值组成的内存块,这些值用零初始化,如下图所示:
var image [256]byte
与 C 和 Java 类似,Go 使用方括号索引表达式访问存储在数组变量中的值。这是通过指定变量标识符,后跟方括号内元素的索引来实现的,如以下代码示例所示:
p := [5]int{122,6,23,44,6}
p[4] = 82
fmt.Println(p[0])
前面的代码更新第五个元素并打印数组中的第一个元素。
内置的len
函数返回数组类型的声明长度。内置的cap
函数可用于阵列以返回其容量。例如,在下面的源代码片段中,[7]string
类型的数组seven
将返回7
作为其长度和容量:
func main() {
seven := [7]string{"grumpy", "sleepy", "bashful"}
fmt.Println(len(seven), cap(seven))
}
对于数组,cap()
函数总是返回与len()
相同的值。这是因为数组值的最大容量是其声明的长度。容量函数更适合与切片类型一起使用(本章后面将讨论)。
数组遍历可以使用传统的for
语句或更惯用的for…range
语句来完成。下面的代码片段显示了使用for
语句完成的数组遍历,在init()
中使用随机数初始化数组,以及用于实现max()
函数的for
范围语句:
const size = 1000
var nums [size]int
func init() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < size; i++ {
nums[i] = rand.Intn(10000)
}
}
func max(nums [size]int) int {
temp := nums[0]
for _, val := range nums {
if val > temp {
temp = val
}
}
return temp
}
golang.fyi/ch07/arrmax_iter.go
在传统的for
语句中,循环的索引变量i
使用索引表达式num[i]
访问数组的值。在 AUTT3 席语句中,在 AuthT4@函数中,迭代值存储在循环 Type 的每个 T5 变量中,并且忽略索引(分配给空白标识符)。如果您不了解中的语句是如何工作的,请参阅第 3 章、Go 控制流,了解 Go 中循环机制的详细说明。
数组值被视为单个单位。数组变量不是指向内存中某个位置的指针,而是表示包含数组元素的整个内存块。这意味着当数组变量被重新分配或作为函数参数传入时,将创建数组值的新副本。
这可能会对程序的内存消耗产生不必要的副作用。一种修复方法是使用指针类型引用数组值。在下面的示例中,声明了一个命名类型numbers
,以表示数组类型[1024 * 1024]]int
。函数 initialize()
和max()
没有直接将数组值作为参数,而是接收一个类型为*numbers
的指针,如下面的源代码片段所示:
type numbers [1024 * 1024]int
func initialize(nums *numbers) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < size; i++ {
nums[i] = rand.Intn(10000)
}
}
func max(nums *numbers) int {
temp := nums[0]
for _, val := range nums {
if val > temp {
temp = val
}
}
return temp
}
func main() {
var nums *numbers = new(numbers)
initialize(nums)
}
golang.fyi/ch07/arrptr.go
前面的代码使用内置函数new(numbers)
以零值初始化数组元素,并获得指向该数组的指针,如main()
所示。因此,当调用函数initialize
和max
时,它们将接收阵列的地址(其副本),而不是整个 100K 大小的阵列。
在更改主题之前,应注意,可以使用地址运算符&
初始化复合文字数组值,以初始化并返回数组指针,如以下示例所示。在代码段中,复合文字&galaxies{...}
返回指针*galaxies
,并用指定的元素值初始化:
type galaxies [14]string
func main() {
namedGalaxies = &galaxies{
"Andromeda",
"Black Eye",
"Bode's",
...
}
printGalaxies(namedGalaxies)
}
golang.fyi/ch07/arraddr.go
阵列类型是 Go 中的低级存储结构。例如,阵列通常用作存储原语的基础,其中有严格的内存分配要求以最小化空间消耗。然而,在更常见的情况下,下一节将介绍切片,它通常被用作处理序列索引集合的更惯用方法。
切片类型通常用作 Go 中索引数据的惯用构造。切片比数组更灵活,具有更多有趣的特性。切片本身是一种复合类型,其语义类似于数组。事实上,切片使用数组作为其底层数据存储机制。切片类型的一般形式如下所示:
【】<元件\ U 型>
切片和数组类型之间的一个明显区别是类型声明中省略了大小,如以下示例所示:
var (
image []byte
ids []string
vector []float64
months []string
q1 []string
histogram []map[string]int // slice of map (see map later)
)
golang.fyi/ch07/slicetypes.go
切片类型中缺少的大小属性表示以下情况:
- 与数组不同,切片的大小不是固定的
- 切片类型表示指定元素类型的所有集合
这意味着一个切片在理论上可以无限增长(尽管在实践中这不是真的,因为切片背后有一个底层的有界数组)。给定元素类型的切片被视为同一类型,无论其底层大小如何。这将删除数组中的限制,其中大小决定类型。
例如,以下变量months
和q1
具有相同的[]string
类型,并且将毫无问题地编译:
var (
months []string
q1 []string
)
func print(strs []string){ ... }
func main() {
print(months)
print(q1)
}
golang.fyi/ch07/slicetypes.go
与数组类似,切片类型可以嵌套以创建多维切片,如以下代码段所示。每个维度可以独立地具有自己的大小,并且必须单独初始化:
var(
board [][]int
graph [][][][]int
)
类型系统将切片表示为值(下一节将探讨切片的内部表示)。但是,与数组类型不同,未初始化的片具有零值nil,这意味着任何访问未初始化片元素的尝试都将导致程序死机。
初始化切片的最简单方法之一是使用以下格式的复合文字值(类似于数组):
<切片\ U 型>{<元素值逗号分隔列表>}
切片的文本值由切片类型和一组逗号分隔的值组成,这些值用花括号括起来,分配给切片的元素。以下代码段说明了使用复合文字值初始化的几个切片变量:
var (
ids []string = []string{"fe225", "ac144", "3b12c"}
vector = []float64{12.4, 44, 126, 2, 11.5}
months = []string {
"Jan", "Feb", "Mar", "Apr",
"May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec",
}
// slice of map type (maps are covered later)
tables = []map[string][]int {
{
"age":{53, 13, 5, 55, 45, 62, 34, 7},
"pay":{124, 66, 777, 531, 933, 231},
},
}
graph = [][][][]int{
{{{44}, {3, 5}}, {{55, 12, 3}, {22, 4}}},
{{{22, 12, 9, 19}, {7, 9}}, {{43, 0, 44, 12}, {7}}},
}
)
golang.fyi/ch07/sliceinit.go
如前所述,切片的复合文字值使用与数组类似的形式表示。但是,文本中提供的元素数量不受固定大小的限制。这意味着文本可以根据需要任意大。不过,在封面下,Go 创建并管理一个大小合适的数组来存储文本中表示的值。
前面提到,slice 值使用底层数组存储数据。名称切片实际上是对数组中数据段切片的引用。在内部,切片由具有以下三个属性的复合值表示:
| **属性** | **说明** | | 一个*指针* | 指针是存储在底层数组中的片的第一个元素的地址。当切片值未初始化时,其指针值为 nil,表示它尚未指向数组。Go 使用指针作为切片本身的零值。未初始化的片将返回 nil 作为其零值。但是,类型系统不会将切片值视为参考值。这意味着某些函数可以应用于 nil 片,而其他函数则会引起恐慌。创建切片后,指针不会更改。要指向不同的起点,必须创建一个新切片。 | | a*长度* | 长度表示可以从第一个元素开始访问的连续元素的数量。它是一个动态值,可以增长到片的容量(请参见下一页的容量)。切片的长度始终小于或等于其容量。如果试图访问超出切片长度的元素而不调整大小,将导致死机。即使容量大于长度,也是如此。 | | a*容量* | 切片的容量是从第一个元素开始,可以存储在切片中的最大元素数。片的容量受底层数组的长度限制。 |因此,当如下变量halfyr
初始化时,如图所示:
halfyr := []string{"Jan","Feb","Mar","Apr","May","Jun"}
它将存储在类型为[6]string
的数组中,指针指向第一个元素,长度和容量为6
,如下图所示:
创建切片值的另一种方法是切片现有数组或其他切片值(或指向这些值的指针)。Go 提供了一种索引格式,可以方便地表示切片操作,如下所示:
<切片或数组值><低 U 指数><高 U 指数>
切片表达式使用[[T0]运算符为切片段指定由冒号分隔的下限索引和上限索引。
- 低值是切片段开始的从零开始的索引
- 高值是段停止处的*nth*元素偏移量
下表显示了通过对以下值重新切片的切片表达式示例:halfyr := []string{"Jan","Feb","Mar","Apr","May","Jun"}
。
对现有切片或数组值进行切片不会创建新的基础数组。新切片创建指向基础数组的新指针位置。例如,下面的代码显示了将切片值halfyr
切片为两个额外的切片:
var (
halfyr = []string{
"Jan", "Feb", "Mar",
"Apr", "May", "Jun",
}
q1 = halfyr[:3]
q2 = halfyr[3:]
)
golang.fyi/ch07/slice\u reslice.go
背衬阵列可能有许多片投影其数据的特定视图。下图说明了如何直观地表示前面代码中的切片:
请注意,q1
和q2
都指向同一底层数组中的不同元素。切片q1
的初始长度为3
,容量为6
。这意味着q1
总共可以调整到6
元素的大小。但是,片[T7]的大小为[T8],容量为[T9],不能超过其初始大小(片大小调整将在后面介绍)。
如前所述,数组也可以直接切片。在这种情况下,提供的数组值将成为基础数组。将使用提供的数组计算切片的容量和长度。以下源代码片段显示了名为“月”的现有数组值的切片:
var (
months [12]string = [12]string{
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}
halfyr = months[:6]
q1 = halfyr[:3]
q2 = halfyr[3:6]
q3 = months[6:9]
q4 = months[9:]
)
golang.fyi/ch07/slice\u reslice\u arr.go
最后,Go 的切片表达式支持更长的形式,其中切片的最大容量包含在表达式中,如下所示:
<切片或数组的值><低指数>:<高指数>:最大值】
max属性指定要用作新片的最大容量的索引值。该值可能小于或等于基础阵列的实际容量。以下示例对包含最大值的数组进行切片:
var (
months [12]string = [12]string{
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}
summer1 = months[6:9:9]
)
golang.fyi/ch07/slice\u reslice\u arr.go
前面的代码片段创建了一个新的切片值summer1
,大小为3
(从索引位置6
到9
开始)。最大索引设置为位置9
,这意味着该片的容量为3
。如果未指定最大值,则最大容量将自动设置为基础阵列的最后一个位置,与以前一样。
可以使用内置函数make
在运行时初始化切片。此函数创建一个新的切片值,并使用元素类型的零值初始化其元素。未初始化的切片的值为零,表示它没有指向底层数组。如果没有显式初始化,使用复合文字值或使用make()
函数,尝试访问切片元素将导致恐慌。下面的代码段重写了前面的示例,使用make()
函数初始化切片:
func main() {
months := make([]string, 6)
...
}
golang.fyi/ch07/slicemake.go
make()
函数将待初始化切片的类型和切片的初始大小作为参数。然后它返回一个切片值。在前面的片段中,make()
执行以下操作:
- 创建类型为
[6]string
的基础数组 - 创建长度和容量为
6
的切片值 - 返回一个切片值(不是指针)
使用make()
函数初始化后,对合法索引位置的访问将返回切片元素的零值,而不会导致程序死机。make()
函数可以采用可选的第三个参数,指定片的最大容量,如下例所示:
func main() {
months := make([]string, 6, 12)
...
}
golang.fyi/ch07/slicemake2.go
前面的代码段将使用初始长度为6
且最大容量为12
的切片值初始化months
变量。
处理切片值的最简单操作是访问其元素。如前所述,切片使用索引表示法访问其元素,类似于数组。以下示例访问索引位置 0 处的元素并更新为15
:
func main () {
h := []float64{12.5, 18.4, 7.0}
h[0] = 15
fmt.Println(h[0])
...
}
golang.fyi/ch07/slice\u use.go
程序运行时,使用索引表达式h[0]
打印更新后的值,以检索位置0
处项目的值。请注意,仅包含索引号的切片表达式(例如,【T2)】返回该位置的项的值。然而,当表达式包含冒号时,例如h[2:]
或h[:6]
,该表达式将返回一个新切片。
切片遍历可以使用传统的for
语句或更惯用的for…range
语句完成,如以下代码段所示:
func scale(factor float64, vector []float64) []float64 {
for i := range vector {
vector[i] *= factor
}
return vector
}
func contains(val float64, numbers []float64) bool {
for _, num := range numbers {
if num == val {
return true
}
}
return false
}
golang.fyi/ch07/slice_loop.go
在前面的代码片段中,函数scale
使用索引变量i
直接更新切片factor
中的值,而函数contains
使用num
中存储的迭代发出值访问切片元素。如果您需要有关for…range
语句的更多详细信息,请参阅第三章、Go 控制流。
当函数接收一个切片作为其参数时,该切片的内部指针指向该切片的底层数组。因此,函数的调用者将看到函数内对切片的所有更新。例如,在下面的代码段中,vector
参数的所有更改都将被函数scale
的调用者看到:
func scale(factor float64, vector []float64) {
for i := range vector {
vector[i] *= factor
}
}
golang.fyi/ch07/slice_loop.go
Go 提供了两个内置函数来查询切片的长度和容量属性。给定一个切片,可以分别使用len
和cap
函数查询其长度和最大容量,如下例所示:
func main() {
var vector []float64
fmt.Println(len(vector)) // prints 0, no panic
h := make([]float64, 4, 10)
fmt.Println(len(h), ",", cap(h))
}
回想一下,切片是一个值(不是指针),它的零值是 nil。因此,代码能够查询未初始化切片的长度(和容量),而不会在运行时引起恐慌。
切片类型的一个不可或缺的特性是其动态增长的能力。默认情况下,切片具有静态长度和容量。任何试图访问超过该限制的索引的行为都会引起恐慌。Go 提供了内置的可变函数append
,可以根据需要动态地向指定切片添加新值,从而增加切片的长度和容量。以下代码段显示了如何完成此操作:
func main() {
months := make([]string, 3, 3)
months = append(months, "Jan", "Feb", "March",
"Apr", "May", "June")
months = append(months, []string{"Jul", "Aug", "Sep"}...)
months = append(months, "Oct", "Nov", "Dec")
fmt.Println(len(months), cap(months), months)
}
golang.fyi/ch07/slice_append.go
前面的代码段以一个大小和容量为3
的切片开始。append
函数用于在片的初始大小和容量之外向片动态添加新值。在内部,append
将尝试在目标切片内拟合附加值。如果切片尚未初始化或容量不足,append 将分配一个新的底层数组来存储更新切片的值。
回想一下,分配或切片现有切片值只会创建一个指向同一底层数组结构的新切片值。Go 提供了copy
函数,该函数返回切片的深度副本以及一个新的底层数组。下面的代码片段显示了一个clone()
函数,该函数生成一段数字的新副本:
func clone(v []float64) (result []float64) {
result = make([]float64, len(v), cap(v))
copy(result, v)
return
}
golang.fyi/ch07/slice\u use.go
在前面的代码段中,copy
函数将v
切片的内容复制到result
中。源和目标切片必须具有相同的大小和类型,否则复制操作将失败。
在内部,字符串类型被实现为一个片段,使用一个指向符文的底层数组的复合值。这为字符串类型提供了与切片相同的惯用处理方法。例如,以下代码段使用索引表达式从给定字符串值提取字符串片段:
func main() {
msg := "Bobsayshelloworld!"
fmt.Println(
msg[:3], msg[3:7], msg[7:12],
msg[12:17], msg[len(msg)-1:],
)
}
golang.fyi/ch07/slice_string.go
字符串上的切片表达式将返回一个指向其基础符文数组的新字符串值。字符串值可以转换为字节片(或符文片),如下面的函数片段所示,它对给定字符串的字符进行排序:
func sort(str string) string {
bytes := []byte(str)
var temp byte
for i := range bytes {
for j := i + 1; j < len(bytes); j++ {
if bytes[j] < bytes[i] {
temp = bytes[i]
bytes[i], bytes[j] = bytes[j], temp
}
}
}
return string(bytes)
}
golang.fyi/ch07/slice_string.go
前面的代码显示了字节片到字符串值的显式转换。请注意,可以使用索引表达式访问每个字符。
Go 映射是一种复合类型,用作存储由任意键值索引的同一类型无序元素的容器。以下代码段显示了具有各种键类型的各种映射变量声明:
var (
legends map[int]string
histogram map[string]int
calibration map[float64]bool
matrix map[[2][2]int]bool // map with array key type
table map[string][]string // map of string slices
// map (with struct key) of map of string
log map[struct{name string}]map[string]string
)
golang.fyi/ch07/maptypes.go
前面的代码片段显示了几个声明为具有各种键类型的不同类型映射的变量。通常,贴图类型指定如下:
地图<键><元素>
键指定用于索引地图存储元素的值的类型。与数组和切片不同,映射键可以是任何类型,而不仅仅是[T0]。但是,映射键必须是可比较的类型,包括数字、字符串、布尔值、指针、数组、结构和接口类型(有关可比较类型的讨论,请参见第 4 章、数据类型)。
与切片类似,映射管理对其用户不透明的底层数据结构来存储其值。未初始化的映射也具有零值。尝试插入未初始化的映射将导致程序死机。但是,与切片不同,可以从 nil 映射访问元素,该映射将返回元素的零值。
与其他复合类型一样,可以使用以下形式的复合文字值初始化映射:
<映射类型>{<以逗号分隔的键:值对列表>}
以下代码段显示了使用映射复合文字的变量初始化:
var (
histogram map[string]int = map[string]int{
"Jan":100, "Feb":445, "Mar":514, "Apr":233,
"May":321, "Jun":644, "Jul":113, "Aug":734,
"Sep":553, "Oct":344, "Nov":831, "Dec":312,
}
table = map[string][]int {
"Men":[]int{32, 55, 12, 55, 42, 53},
"Women":[]int{44, 42, 23, 41, 65, 44},
}
)
golang.fyi/ch07/mapinit.go
文字映射值是使用冒号分隔的键和值对指定的,如前一个示例所示。每个键和值对的类型必须与映射中声明的元素的类型匹配。
与切片类似,映射值也可以使用生成函数进行初始化。使用 make 函数初始化基础存储,允许在映射中插入数据,如以下简短片段所示:
func main() {
hist := make(map[int]string)
hist["Jan"] = 100
hist["Feb"] = 445
hist["Mar"] = 514
...
}
golang.fyi/ch07/maptypes.go
make
函数以映射的类型作为参数,并返回一个初始化的映射。在前面的示例中,make
函数将初始化类型为map[int]string
的映射。make
函数可以选择使用第二个参数来指定地图的容量。但是,映射将根据需要继续增长,忽略指定的初始容量。
与切片和数组一样,索引表达式用于访问和更新存储在映射中的元素。要设置或更新map
元素,请使用赋值左侧的索引表达式指定要更新的元素的键。以下代码段显示了一个元素,该元素的[T1]键被更新为值[T2]:
hist := make(map[int]string)
hist["Jan"] = 100
使用给定键访问元素是通过索引表达式完成的,该表达式位于赋值的右侧,如以下示例所示,其中使用"Mar"
键索引的值被赋值为val
变量:
val := hist["Mar"]
前面提到,访问不存在的键将返回该元素的零值。例如,如果映射中不存在键为[T0]的元素,则前面的代码将返回 0。你可以想象,这可能是个问题。您如何知道您得到的是实际值还是零值?幸运的是,Go 提供了一种显式测试是否缺少元素的方法,它返回一个可选的布尔值作为索引表达式结果的一部分,如以下代码段所示:
func save(store map[string]int, key string, value int) {
val, ok := store[key]
if !ok {
store[key] = value
}else{
panic(fmt.Sprintf("Slot %d taken", val))
}
}
golang.fyi/ch07/map of use.go
前面代码段中的函数在更新键的值之前测试键的存在性。称为逗号 ok习惯用法,ok
变量中存储的布尔值在实际找不到值时设置为 false。这允许代码区分缺少键和元素的零值。
for…range
循环语句可用于遍历映射值的内容。range
表达式在每次迭代中同时为键值和元素值发出值。下面的代码片段显示了对 maphist
的遍历:
for key, val := range hist {
adjVal := int(float64(val) * 0.100)
fmt.Printf("%s (%d):", key, val)
for i := 0; i < adjVal; i++ {
fmt.Print(".")
}
fmt.Println()
}
golang.fyi/ch07/map of use.go
每次迭代都返回一个键及其关联的元素值。然而,不能保证迭代顺序。内部映射迭代器可以在程序每次运行时以不同的顺序遍历映射。为了保持可预测的遍历顺序,请在单独的结构(例如切片)中保留(或生成)键的副本。在遍历过程中,以可预测的方式遍历关键帧片的范围。
您应该知道,在迭代过程中对发出的值所做的更新将丢失。相反,使用索引表达式,例如hist[key]
在迭代期间更新元素。关于for…range
回路的详细说明,请参见第 3 章、Go 控制流程,详细说明 Gofor
回路。
除了前面讨论的make
函数外,映射类型还支持下表中讨论的两个附加函数:
h := map[int]bool{3:true, 7:false, 9:false}
fmt.Println(len(h))
对于未初始化的映射,len
函数将返回零。 |
| 删除(地图、钥匙) | 内置的delete
函数从与提供的键相关联的给定映射中删除元素。以下代码段将打印2:
h := map[int]bool{3:true, 7:false, 9:false}
delete(h,7)
fmt.Println(len(h))
|
因为映射维护一个指向其备份存储结构的内部指针,所以一旦函数返回,调用方将看到对被调用函数中映射参数的所有更新。下面的示例显示了对remove
函数的调用,以更改地图的内容。一旦remove
函数返回,传递的变量hist
将反映变化:
func main() {
hist := make(map[string]int)
hist["Jun"] = 644
hist["Jul"] = 113
remove(hit, "Jun")
len(hist) // returns 1
}
func remove(store map[string]int, key string) error {
_, ok := store[key]
if !ok {
return fmt.Errorf("Key not found")
}
delete(store, key)
return nil
}
golang.fyi/ch07/map of use.go
本章讨论的最后一种类型是 Go 的struct
。它是一个复合类型,用作其他命名类型(称为字段)的容器。以下代码段显示了几个声明为结构的变量:
var(
empty struct{}
car struct{make, model string}
currency struct{name, country string; code int}
node struct{
edges []string
weight int
}
person struct{
name string
address struct{
street string
city, state string
postal string
}
}
)
golang.fyi/ch07/structtypes.go
请注意,结构类型具有以下常规格式:
结构{<字段声明集>}
struct
类型是通过指定关键字struct
和一组用花括号括起来的字段声明来构造的。在其最常见的形式中,字段是一个具有指定类型的唯一标识符,它遵循 Go 的变量声明约定,如前面的代码片段所示(struct
还支持匿名字段,稍后将介绍)。
理解struct
的类型定义包括其所有声明的字段是至关重要的。例如,person 变量的类型(参见前面的代码片段)是声明struct { name string; address struct { street string; city string; state string; postal string }}
中的整个字段集。因此,任何需要该类型的变量或表达式都必须重复该长声明。稍后我们将看到如何通过为struct
使用命名类型来缓解这种情况。
结构使用选择器表达式(或点表示法)访问存储在字段中的值。例如,下面将打印前面代码段中 person struct 变量的name
字段的值:
fmt.Pritnln(person.name)
选择器可以链接到嵌套在结构中的访问字段。以下代码段将打印[T0]变量嵌套地址值的街道和城市:
fmt.Pritnln(person.address.street)
fmt.Pritnln(person.address.city)
与阵列类似,结构是纯值,没有额外的底层存储结构。未初始化结构的字段被分配各自的零值。这意味着未初始化的结构不需要进一步分配,可以使用。
但是,可以使用以下形式的复合文字来显式初始化结构变量:
<结构类型>{<位置或命名字段值>}
结构的复合文字值可以由一组由其各自位置指定的字段值初始化。使用这种方法,必须提供所有字段值,以匹配其各自声明的类型,如以下代码段所示:
var(
currency = struct{
name, country string
code int
}{
"USD", "United States",
840,
}
...
)
golang.fyi/ch07/structinit.go
在前面的 struct literal 中,struct
的所有字段值都被提供,与它们声明的字段类型相匹配。或者,可以使用字段索引及其关联值来指定[T1]的复合文字值。与前面一样,索引(字段名)及其值由冒号分隔,如以下代码段所示:
var(
car = struct{make, model string}{make:"Ford", model:"F150"}
node = struct{
edges []string
weight int
}{
edges: []string{"north", "south", "west"},
}
...
)
golang.fyi/ch07/structinit.go
如您所见,当提供索引及其值时,可以有选择地指定复合文字的字段值。例如,在node
变量的初始化中,edge
字段被初始化,而weight
被省略。
尝试重用结构类型可能会很快变得笨拙。例如,每次需要时,必须编写[T0]来表示结构类型,这将不可扩展,容易出错,并且会让脾气暴躁的 Go 开发人员难以接受。幸运的是,解决这一问题的正确习惯用法是使用命名类型,如以下源代码片段所示:
type person struct {
name string
address address
}
type address struct {
street string
city, state string
postal string
}
func makePerson() person {
addr := address{
city: "Goville",
state: "Go",
postal: "12345",
}
return person{
name: "vladimir vivien",
address: addr,
}
}
golang.fyi/ch07/structtype_dec.go
上一个示例将结构类型定义绑定到标识符 person 和 address。这允许在不同的上下文中重用结构类型,而无需进行冗长的类型定义。您可以参考第 4 章、数据类型,了解命名类型的更多信息。
以前的结构类型定义涉及命名字段的使用。但是,也可以仅使用字段类型定义字段,而忽略标识符。这称为匿名字段。它的作用是将类型直接嵌入到结构中。
下面的代码片段演示了这个概念。两种类型diameter
和名称都作为anonymous
字段嵌入在planet
类型中:
type diameter int
type name struct {
long string
short string
symbol rune
}
type planet struct {
diameter
name
desc string
}
func main() {
earth := planet{
diameter: 7926,
name: name{
long: "Earth",
short: "E",
symbol: '\u2641',
},
desc: "Third rock from the Sun",
}
...
}
golang.fyi/ch07/struct_embed.go
前面代码段中的main
函数显示了如何访问和更新匿名字段,正如在planet
结构中所做的那样。请注意,嵌入类型的名称将成为结构的复合文字值中的字段标识符。
要简化字段名解析,请在使用匿名字段时遵循以下规则:
- 类型的名称将成为字段的名称
- 匿名字段的名称不能与其他字段名称冲突
- 仅使用导入类型的非限定(忽略包)类型名称
当直接使用选择器表达式访问嵌入式结构的字段时,这些规则也适用,如下面的代码段所示。请注意,嵌入类型的名称解析为字段名称:
func main(){
jupiter := planet{}
jupiter.diameter = 88846
jupiter.name.long = "Jupiter"
jupiter.name.short = "J"
jupiter.name.symbol = '\u2643'
jupiter.desc = "A ball of gas"
...
}
golang.fyi/ch07/struct_embed.go
嵌入结构的字段可以提升为其封闭类型。提升的字段出现在选择器表达式中,但没有其类型的限定名称,如下例所示:
func main() {
...
saturn := planet{}
saturn.diameter = 120536
saturn.long = "Saturn"
saturn.short = "S"
saturn.symbol = '\u2644'
saturn.desc = "Slow mover"
...
}
golang.fyi/ch07/struct_embed.go
在前面的代码段中,高亮显示的字段是从嵌入类型name
升级而来的,方法是从选择器表达式中省略它。字段long
、short
和symbol
的值来自嵌入式类型name
。同样,这仅在升级不会导致任何标识符冲突的情况下才有效。在出现歧义的情况下,可以使用完全限定的选择器表达式。
回想一下,结构变量存储实际值。这意味着每当[T0]变量被重新分配或作为函数参数传入时,就会创建结构值的新副本。例如,在调用updateName()
后,以下内容不会更新 name 的值:
type person struct {
name string
title string
}
func updateName(p person, name string) {
p.name = name
}
func main() {
p := person{}
p.name = "uknown"
...
updateName(p, "Vladimir Vivien")
}
golang.fyi/ch07/struct_ptr.go
这可以通过传递指向 person 类型的struct
值的指针来解决,如以下代码段所示:
type person struct {
name string
title string
}
func updateName(p *person, name string) {
p.name = name
}
func main() {
p := new(person)
p.name = "uknown"
...
updateName(p, "Vladimir Vivien")
}
golang.fyi/ch07/struct_ptr2.go
在此版本中,p
变量被声明为*person
,并使用内置的new()
函数进行初始化。updateName()
返回后,调用函数会看到其变化。
关于结构的最后一个主题与字段标记有关。在定义struct
类型的过程中,可以向每个字段声明添加可选的string
值。字符串的值是任意的,它可以作为使用反射来使用标记的工具或其他 API 的提示。
下面显示了使用 JSON 注释标记的 Person 和 Address 结构的定义,可以由 Go 的 JSON 编码器和解码器(在标准库中找到)进行解释:
type Person struct {
Name string `json:"person_name"`
Title string `json:"person_title"`
Address `json:"person_address_obj"`
}
type Address struct {
Street string `json:"person_addr_street"`
City string `json:"person_city"`
State string `json:"person_state"`
Postal string `json:"person_postal_code"`
}
func main() {
p := Person{
Name: "Vladimir Vivien",
Title : "Author",
...
}
...
b, _ := json.Marshal(p)
fmt.Println(string(b))
}
golang.fyi/ch07/struct_ptr2.go
请注意,标记表示为原始字符串值(包装在一对[T0]中)。正常代码执行会忽略这些标记。但是,可以像 JSON 库一样使用 Go 的反射 API 来收集它们。当本书讨论输入和输出流时,您将在第 10 章中Go中的数据 IO 中遇到更多关于此主题的内容。
本章介绍了 Go 中发现的每一种复合类型,深入介绍了它们的特性,涵盖了很多方面。本章首先介绍了数组类型,在这里读者学习了如何声明、初始化和使用数组值。接下来,读者了解了有关切片类型的所有信息,特别是使用切片索引表达式创建新切片或重新切片现有切片的声明、初始化和实际示例。本章介绍了地图类型,其中包括有关地图初始化、访问、更新和遍历的信息。最后,本章提供了有关结构类型的定义、初始化和使用的信息。
不用说,这可能是本书最长的章节之一。然而,随着本书不断探索新的主题,这里所涵盖的信息将被证明是非常宝贵的。下一章将介绍使用 Go 来支持使用方法和接口的类对象习惯用法的思想。