在前面的内容中,我们已经掌握了Go语言中的基本类型,这些类型就像是搭建数据世界的“原子”。接下来,我们要一起认识更复杂的“分子”——也就是由基本类型组合而成的复合类型。 本部分我们会聚焦于四种常用的复合类型:数组、切片、映射(map)和结构体(struct)。
我们可以把数组和结构体看作是“聚合类型”,它们的本质是把多个值拼接在一起。数组的特点是所有元素类型一致,比如一排整齐的数字;结 构体则可以包含不同类型的字段,就像一个学生信息表里既有名字也有年龄。需要注意的是,数组和结构体的大小在定义时就已经确定,不能随意变长。 而切片和映射则灵活得多,随着我们不断添加元素,它们会自动扩展,非常适合处理动态数据。

数组是由零个或多个相同类型元素组成的固定长度序列。正因为它们的长度固定,数组在Go语言中很少被直接使用。切片具有可增长和缩小的特性,使用起来更加灵活。但是,要理解切片,我们必须首先掌握数组的概念。
在Go语言中,数组的每个元素都可以通过下标(也叫索引)来访问。下标是从0开始的,也就是说,第一个元素的下标是0,最后一个元素的下标是数组长度减一。 比如说,如果我们有一个长度为5的数组,那么它的下标范围就是0到4。我们只需要写上数组名和方括号里的下标,就能直接读取或修改对应位置的元素。
|var nums [5]int // 声明一个包含5个整数的数组 fmt.Println(nums[0]) // 输出第一个元素 fmt.Println(nums[len(nums)-1]) // 输出最后一个元素
|0 0
在Go语言中,如果我们想要依次访问数组里的每一个元素,最常用的方法就是配合关键字来遍历。 通过,我们不仅可以同时拿到每个元素的下标(索引)和对应的值,还可以根据实际需求只获取值而忽略索引。
rangerange|// 同时获取索引和值 for index, value := range nums { fmt.Printf("索引:%d,值:%d\n", index, value) } // 只获取值,忽略索引 for _, value := range nums { fmt.Printf("值:%d\n", value) }
当我们新声明一个数组变量时,Go会自动把数组里的每个元素都设置为该类型的零值。比如说,如果数组元素是整数类型,那么每个元素的初始值就是0;如果是字符串类型,则每个元素默认是空字符串。 这样做的好处是,我们不用担心数组里会有“脏数据”。
当然,实际开发中我们经常需要给数组赋一些具体的初始值。Go提供了数组字面量的写法,让我们可以在声明数组的同时,把每个元素的值都写出来。例如:
|var scores [3]int = [3]int{85, 92, 78} var grades [3]int = [3]int{90, 85} // 第三个元素自动为0 fmt.Println(grades[2]) // 输出:0
在数组字面量中,如果长度位置使用省略号"...",那么数组长度将由初始化器的数量决定:
|temperatures := [...]float64{23.5, 25.1, 22.8} fmt.Printf("数组类型:%T\n", temperatures) // 输出:[3]float64
在Go语言中,数组的长度其实是类型定义的一部分。也就是说,像[3]int和[4]int这样的数组,它们虽然元素类型都是int,但因为长度不同,所以被视为两种完全不同的类型。
我们不能把一个长度为3的int数组直接赋值给一个长度为4的int数组,反之亦然。
此外,数组的长度必须是一个常量表达式,也就是在代码编译阶段就能确定具体数值的表达式。 比如,我们可以用字面量、常量或者常量表达式来指定数组长度,但不能用变量。这样做的好处是,编译器可以提前检查数组的边界,帮助我们避免越界等错误。
举个例子:
|scores := [3]int{85, 92, 78} // scores = [4]int{85, 92, 78, 88} // 编译错误:无法将[4]int赋值给[3]int
除了按顺序列出每个元素的值之外,Go还提供了一种更灵活的数组初始化方式:我们可以明确指定某些索引位置对应的值,而不必按照从0开始的连续顺序来填充。 这种方式在处理稀疏数组(大部分元素为零值,只有少数特定位置有意义值的数组)时特别有用。
使用这种索引指定的初始化方式,我们可以跳过某些位置,只为需要的索引设置值,其余位置会自动填充该类型的零值。这样既节省了代码量,又让初始化的意图更加清晰明确:
|type Weekday int const ( Monday Weekday = iota Tuesday Wednesday Thursday Friday ) workHours := [...]int{Monday: 8, Wednesday: 6, Friday: 7} fmt.Println(Wednesday, workHours[Wednesday]) // 输出:2 6
使用这种按索引初始化的方式时,我们有很大的自由度。首先,索引的顺序完全可以打乱——我们可以先指定索引5的值,再指定索引1的值,然后是索引10的值,Go编译器会自动把它们放到正确的位置上。 其次,我们完全可以跳过一些索引不给它们赋值,那些被跳过的位置会自动使用该类型的零值来填充。
这种特性让我们在处理一些特殊场景时非常方便。比如,我们想创建一个很大的数组,但只有最后几个位置需要特定的值,其他位置都保持零值就行。在这种情况下,我们就不需要写出一长串的零值,只需要指定那几个有意义的索引位置即可。例如:
|largeArray := [...]int{99: -1}
这定义了一个包含100个元素的数组,除了最后一个元素值为-1外,其余都是0。
Go语言中的数组比较功能非常强大且直观。当数组的元素类型支持比较操作时(比如数字、字符串、布尔值等基本类型),整个数组类型也自动获得了比较能力。
我们可以直接使用==运算符来比较两个相同类型的数组。这个比较过程是逐元素进行的:Go会从第一个元素开始,依次比较两个数组对应位置上的每个元素,只有当所有对应位置的元素都相等时,整个数组才被认为是相等的。
如果任何一个位置的元素不相等,整个比较就会返回false。
相应地,!=运算符是==的否定形式,当两个数组不完全相同时返回true。
|first := [2]int{1, 2} second := [...]int{1, 2} third := [2]int{1, 3} fmt.Println(first == second, first == third, second == third) // 输出:true false false
当函数被调用时,每个参数值的副本都会赋值给对应的参数变量,所以函数接收到的是副本,而不是原始值。以这种方式传递大型数组可能会很低效,而且函数对数组元素的任何修改都只会影响副本,不会影响原始数组。
如果我们需要让函数能够修改数组的内容,可以显式传递数组的指针。下面是一个将32字节数组内容清零的函数:
|func clearArray(ptr *[32]byte) { for i := range ptr { ptr[i] = 0 } }
我们也可以利用数组字面量[32]byte{}会产生一个所有元素都为零的32字节数组这一特点,写出更简洁的版本:
|func clearArray(ptr *[32]byte) { *ptr = [32]byte{} }
使用数组指针是高效的,并且允许被调用的函数修改调用者的变量。但是,由于数组的固定大小特性,它们仍然缺乏灵活性。
例如,上面的clearArray函数无法接受指向[16]byte变量的指针,也没有办法添加或删除数组元素。
切片表示可变长度的序列,其中所有元素都具有相同的类型。切片类型写作[]T,其中元素类型为T。它看起来像没有大小的数组类型。
数组和切片之间有着密切的联系。切片是一个轻量级的数据结构,它提供了对数组中某个子序列(或可能是全部)元素的访问,这个数组被称为切片的底层数组。
切片有三个组成部分:指针、长度和容量。指针指向可以通过切片访问的数组的第一个元素,这不一定是数组的第一个元素。长度是切片元素的数量;
它不能超过容量,容量通常是从切片开始到底层数组末尾的元素数量。内置函数len和cap分别返回这些值。
多个切片可以共享同一个底层数组,并且可能引用该数组的重叠部分。让我们创建一个表示一年中月份的数组示例:
|months := [...]string{1: "一月", 2: "二月", 3: "三月", 4: "四月", 5: "五月", 6: "六月", 7: "七月", 8: "八月", 9: "九月", 10
在这里,一月是months[1],十二月是months[12]。通常数组索引0处的元素会包含第一个值,但由于月份总是从1开始编号,我们可以将索引0留空,它会被初始化为空字符串。
切片操作符s[i:j](其中0 ≤ i ≤ j ≤ cap(s))创建一个新切片,它引用序列s中从i到j-1的元素,s可以是数组变量、数组指针或另一个切片。
结果切片包含j-i个元素。如果省略i,则默认为0;如果省略j,则默认为len(s)。
|secondQuarter := months[4:7] // 第二季度 summer := months[6:9] // 夏季月份 fmt.Println(secondQuarter) // 输出:["四月" "五月" "六月"] fmt.Println(summer) // 输出:["六月" "七月" "八月"]
|["四月" "五月" "六月"] ["六月" "七月" "八月"]
注意"六月"同时包含在两个切片中。我们可以通过以下代码找到共同的元素:
|for _, s := range summer { for _, q := range secondQuarter { if s == q { fmt.Printf("%s在两个季度中都出现\n", s) } } }
|六月在两个季度中都出现
切片的边界检查遵循特定的规则。当我们使用切片操作符s[i:j]时,如果j超出了切片的容量cap(s),程序会在运行时引发panic错误,因为这会尝试访问底层数组范围之外的内存。
但是,如果j只是超出了切片的长度len(s)而仍在容量范围内,切片操作是允许的,这会创建一个比原始切片更长的新切片,有效地"扩展"了我们可以访问的元素范围。这种机制让我们能够重新获得之前被截断但仍在底层数组中的元素:
|// fmt.Println(summer[:20]) // panic: 超出范围 extendedSummer := summer[:5] // 在容量范围内扩展切片 fmt.Println(extendedSummer) // 输出:["六月" "七月" "八月" "九月" "十月"]
值得注意的是,字符串的子串操作与
[]byte切片的切片操作非常相似。两者都写作x[m:n],都返回原始字节的子序列,共享底层表示,因此两种操作都需要常数时间。
由于切片包含指向数组元素的指针,将切片传递给函数允许函数修改底层数组元素。换句话说,复制切片会为底层数组创建别名。下面的reverse函数就地反转[]int切片的元素:
|func reverse(s []int) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } } // 使用示例 numbers := [...]int{0,
|[5 4 3 2 1 0]
利用reverse函数,我们可以巧妙地实现数组的左旋转。将切片向左旋转n个位置的简单方法是三次应用reverse函数:首先对前n个元素,然后对剩余元素,最后对整个切片。
|data := []int{0, 1, 2, 3, 4, 5} // 将data向左旋转两个位置 reverse(data[:2]) // 反转前两个元素 reverse(data[2:]) // 反转剩余元素 reverse(data) // 反转整个切片 fmt.Println(data) // 输出:[2 3 4 5 0 1]
|[2 3 4 5 0 1]
注意初始化切片s的表达式与数组a的表达式不同。切片字面量看起来像数组字面量,是由逗号分隔并用大括号包围的值序列,但没有给出大小。这隐式创建了一个合适大小的数组变量,并产生指向它的切片。
与数组不同,切片不能直接比较,因此我们不能使用==来测试两个切片是否包含相同的元素。标准库为比较两个字节切片([]byte)提供了高度优化的bytes.Equal函数,但对于其他类型的切片,我们必须自己进行比较:
|func equal(x, y []string) bool { if len(x) != len(y) { return false } for i := range x { if x[i] != y[i] { return false } } return true }
切片不支持比较操作主要有两个原因。首先,与数组元素不同,切片的元素是间接的,这使得切片可能包含自身。 其次,由于切片元素是间接的,固定的切片值可能在不同时间包含不同的元素,因为底层数组的内容被修改了。
唯一合法的切片比较是与nil的比较:
|if summer == nil { /* ... */ }
切片类型的零值是nil。nil切片没有底层数组,长度和容量都为零。但也存在长度和容量为零的非nil切片,如[]int{}或make([]int, 3)[3:]。
|var s []int // len(s) == 0, s == nil s = nil // len(s) == 0, s == nil s = []int(nil) // len(s) == 0, s == nil s = []int{} // len(s) == 0, s != nil
如果您需要测试切片是否为空,请使用len(s) == 0,而不是s == nil。
内置函数make可以用来创建切片,它接受元素类型、长度和可选的容量参数。当我们使用make创建切片时,它会在底层分配一个数组,并返回指向该数组的切片。
make函数的语法非常灵活:
make创建的切片中的所有元素都会被初始化为其类型的零值这种创建方式特别适合当我们预先知道切片大小,或者需要为后续的元素添加预留空间的场景。
|make([]T, len) make([]T, len, cap) // 等同于 make([]T, cap)[:len]
在底层,make创建一个未命名的数组变量并返回指向该数组的切片。这个数组变量被分配在堆内存中,我们无法直接访问它——它没有变量名,也无法通过常规的变量声明方式获得引用。
我们只能通过make返回的切片来间接访问和操作这个底层数组中的元素。
这种设计带来了重要的内存管理优势:当切片不再被使用时,Go的垃圾回收器会自动回收这个底层数组占用的内存空间,我们不需要手动管理内存的分配和释放。 同时,由于底层数组是匿名的,多个切片可以安全地共享同一个底层数组的不同部分,而不会产生命名冲突。
内置的append函数是Go语言中最重要的切片操作函数之一,它的主要功能是向现有切片的末尾添加一个或多个元素。这个函数的设计非常智能,它会自动处理底层数组的容量管理,当容量不足时会自动分配更大的数组空间。
append函数的基本语法如下:
append(slice, element) - 向切片添加单个元素append(slice, element1, element2, ...) - 向切片添加多个元素append(slice1, slice2...) - 将一个切片的所有元素添加到另一个切片中需要注意的是,append函数总是返回一个新的切片,即使底层数组没有改变。因此我们通常需要将返回值重新赋值给原变量:
|var chars []rune for _, r := range "你好世界" { chars = append(chars, r) } fmt.Printf("%q\n", chars) // 输出:['你' '好' '世' '界']
|['你' '好' '世' '界']
为了理解append函数的工作原理,让我们看看一个专门用于[]int切片的appendInt版本:
|func appendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x) { // 有空间增长,扩展切片 z = x[:zlen] } else { // 空间不足,分配新数组 // 通过加倍增长,实现摊销线性复杂度 zcap := zlen if
每次调用appendInt都必须检查切片是否有足够的容量在现有数组中容纳新元素。如果有,它通过定义更大的切片(仍在原数组内)来扩展切片。如果增长空间不足,appendInt必须分配一个足够大的新数组来容纳结果。
让我们通过一个程序来观察容量变化的效果:
|func main() { var x, y []int for i := 0; i < 10; i++ { y = appendInt(x, i) fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y) x = y } }
|0 cap=1 [0] 1 cap=2 [0 1] 2 cap=4 [0 1 2] 3 cap=4 [0 1 2 3] 4 cap=8 [0 1 2 3 4] 5 cap=8 [0 1 2 3 4 5] 6 cap=8 [0 1 2 3 4 5 6] 7 cap=8 [0 1 2 3 4 5 6 7] 8 cap=16 [0 1 2 3 4 5 6 7 8] 9 cap=16 [0 1 2 3 4 5 6 7 8 9]
每次容量的变化都表示一次分配和复制操作。内置的append函数可能使用比appendInt更复杂的增长策略。通常我们不知道对append的给定调用是否会引起重新分配,所以我们不能假设原始切片和结果切片引用相同的数组。
内置的append函数允许我们添加多个新元素,甚至整个切片:
|var x []int x = append(x, 1) x = append(x, 2, 3) x = append(x, 4, 5, 6) x = append(x, x...) // 追加切片x本身 fmt.Println(x) // 输出:[1 2 3 4 5 6 1 2 3 4 5 6]
|[1 2 3 4 5 6 1 2 3 4 5 6]
让我们深入探讨更多就地修改切片元素的实用函数。就地修改是一种高效的编程技术,它直接在原有的数据结构上进行操作,而不需要创建新的内存空间。这种方法不仅节省内存,还能提高程序的执行效率。
除了之前提到的rotate和reverse函数,我们还可以实现许多其他有用的就地操作函数。现在让我们来看一个非常实用的例子:nonempty函数。这个函数的作用是从一个字符串切片中移除所有空字符串,只保留有实际内容的字符串。
在实际开发中,我们经常需要处理包含空值或无效数据的列表。比如从用户输入、文件读取或网络请求中获得的数据,可能包含一些空字符串或无效条目。nonempty函数就是专门用来清理这类数据的:
|func nonempty(strings []string) []string { i := 0 for _, s := range strings { if s != "" { strings[i] = s i++ } } return strings[:i] } // 使用示例 data := []string{"apple"
|["apple" "banana" "cherry"] ["apple" "banana" "cherry" "" "cherry"]
需要注意的是,输入切片和输出切片共享同一个底层数组。这避免了分配另一个数组的需要,尽管数据的内容被部分覆盖了。
我们也可以使用append来编写nonempty函数:
|func nonempty2(strings []string) []string { out := strings[:0] // 原始切片的零长度切片 for _, s := range strings { if s != "" { out = append(out, s) } } return out }
要从切片中间删除元素,同时保持剩余元素的顺序,可以使用copy函数将较高索引位置的元素向前滑动来填补被删除元素留下的空隙。这种方法的核心思想是将目标元素后面的所有元素整体向前移动一个位置,从而覆盖掉要删除的元素。
具体的工作原理是:假设我们要删除索引为i的元素,那么索引为i+1及之后的所有元素都需要向前移动一位。copy(slice[i:], slice[i+1:])这个操作会将从slice[i+1:]开始的元素复制到从slice[i:]开始的位置,实现了元素的前移。最后,我们返回一个长度减1的切片slice[:len(slice)-1],这样就完成了元素的删除操作:
|func remove(slice []int, i int) []int { copy(slice[i:], slice[i+1:]) return slice[:len(slice)-1] } // 使用示例 s := []int{10, 20, 30, 40, 50} fmt.
|[10 20 40 50]
如果我们不需要保持顺序,可以简单地将最后一个元素移到空隙中:
|func removeUnordered(slice []int, i int) []int { slice[i] = slice[len(slice)-1] return slice[:len(slice)-1] } // 使用示例 s := []int{10, 20, 30, 40,
|[10 20 50 40]
映射(Map)本质上是基于哈希表这种数据结构实现的。我们可以把哈希表想象成一个超级智能的管理员:无论你给它多少东西要存储,它都能在几乎相同的时间内帮你找到、更新或删除任何一个物品。这就是为什么哈希表被认为是所有数据结构中最巧妙和实用的结构之一。
哈希表的工作原理是将数据组织成键值对的形式。就像我们生活中的字典一样:每个词汇(键)都对应着一个解释(值),而且每个词汇都是独一无二的。哈希表的神奇之处在于,不管字典有多厚(数据有多少),查找任何一个词汇所需的时间都差不多。
在Go语言中,映射就是对哈希表的一种具体实现。当我们看到map[K]V这样的类型声明时,方括号里的K代表键的类型,V代表值的类型。比如map[string]int就表示一个"用字符串作键,用整数作值"的映射。
在Go语言中,我们有多种方法来创建映射。最基础、最通用的方法是使用内置函数make。当我们调用make(map[K]V)时,它会为我们创建一个空的映射,键的类型是K,值的类型是V。
这种方式特别适合在程序运行过程中动态创建映射,或者当我们需要一个空映射然后逐步添加元素时使用:
|ages := make(map[string]int) // 字符串到整数的映射
除了使用make函数创建空映射,我们还有一种更加直观的方式——映射字面量。当我们已经知道映射中需要包含哪些键值对时,这种方式特别有用。
映射字面量的语法非常直观:我们在map[K]V后面跟上一对花括号,花括号里面列出所有的键值对,每个键值对用冒号分隔键和值,不同的键值对之间用逗号分隔。
这种方式让我们可以在声明映射的同时就为它赋予初始数据,特别适合那些在程序启动时就需要预设一些固定数据的场景:
|ages := map[string]int{ "张三": 25, "李四": 30, "王五": 35, }
这等价于:
|ages := make(map[string]int) ages["张三"] = 25 ages["李四"] = 30 ages["王五"] = 35
映射最基本的操作就是存储和读取数据。我们使用下标记号(方括号语法)来访问映射中的元素,这个语法和数组、切片的访问方式看起来很相似,但工作原理有所不同。
当我们使用映射名[键]这样的语法时,Go会在映射中查找对应的键,然后返回与该键关联的值。如果我们在等号左边使用这个语法,就是在为指定的键设置新值;如果在等号右边使用,就是在读取该键对应的值:
|ages["张三"] = 26 fmt.Println(ages["张三"]) // 输出:26
|26
当我们需要从映射中移除某个键值对时,Go语言提供了内置的delete函数。这个函数接受两个参数:第一个是要操作的映射,第二个是要删除的键。
值得注意的是,delete函数即使在指定的键不存在于映射中时也能安全执行,不会引发任何错误或panic:
|delete(ages, "张三") // 删除元素ages["张三"]
映射在处理不存在的键时表现得非常友好和安全,这是它的一个重要特性。当我们尝试访问映射中不存在的键时,Go不会抛出错误或引发panic,而是会返回该值类型对应的零值。
具体来说,如果值类型是整数,那么访问不存在的键会返回0;如果值类型是字符串,则返回空字符串"";如果值类型是布尔值,则返回false。这种设计让我们在编写代码时更加安全,不用时刻担心键是否存在的问题。
正是因为这个特性,下面的代码即使"赵六"这个键在映射中并不存在也能正常工作。当我们第一次执行ages["赵六"]时,由于"赵六"这个键不存在,表达式会返回int类型的零值0。然后0加1等于1,最终"赵六"这个键就被创建并赋值为1了:
|ages["赵六"] = ages["赵六"] + 1 // 生日快乐!
简写赋值形式x += y和x++也适用于映射元素,所以我们可以将上面的语句重写为:
|ages["赵六"] += 1
或者更简洁地写为:
|ages["赵六"]++
但是映射元素不是变量,我们不能获取其地址:
|// _ = &ages["赵六"] // 编译错误:不能获取映射元素的地址
我们不能获取映射元素地址的一个原因是,映射的增长可能会导致现有元素重新哈希到新的存储位置,从而可能使地址无效。
要遍历映射中的所有键值对,我们使用基于range的for循环。这种遍历方式和我们在数组、切片中见过的非常相似,但有一个重要的特点需要注意。
当我们对映射使用range时,每次迭代会得到两个值:第一个是键(key),第二个是对应的值(value)。我们可以在循环体中同时使用这两个信息来处理映射中的每一对数据:
|for name, age := range ages { fmt.Printf("%s\t%d岁\n", name, age) }
|李四 30岁 王五 35岁 赵六 1岁
映射迭代的顺序是未指定的,不同的实现可能使用不同的哈希函数,导致不同的排序。实际上,顺序是随机的,每次执行都会变化。这是有意为之的;使序列变化有助于强制程序在不同实现中保持健壮性。
要按顺序枚举键值对,我们必须显式对键进行排序。如果键是字符串,可以使用sort包中的Strings函数。这是一个常见模式:
|import "sort" var names []string for name := range ages { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Printf("%s\t%d岁\n", name, ages[name]) }
由于我们从一开始就知道names的最终大小,预先分配所需大小的数组会更高效:
|names := make([]string, 0, len(ages))
映射类型的零值是nil,这意味着这个映射变量还没有被初始化,不指向任何实际的哈希表数据结构。换句话说,当我们声明一个映射变量但没有给它分配内存时,它的值就是nil。这种状态下的映射就像是一个空的容器概念,虽然我们可以对它进行一些查询操作,但不能往里面添加任何数据:
|var ages map[string]int fmt.Println(ages == nil) // 输出:true fmt.Println(len(ages) == 0) // 输出:true
|true true
nil映射有一个很重要的特性:虽然它本身并没有指向任何实际的数据结构,但我们仍然可以对它执行大部分的读取操作,而且这些操作都是安全的。
具体来说,当我们对nil映射执行以下操作时,都不会引发任何错误:
ages["张三"],会返回该类型的零值delete(ages, "张三")函数,虽然删除的目标不存在,但程序不会报错len(ages)获取映射大小,会返回0for range循环遍历映射,循环体不会执行任何次数这种设计让nil映射的行为就像一个完全空的映射一样,给我们的编程带来了很大的便利。我们不需要在每次操作前都检查映射是否为nil。
但是,有一个操作是绝对不能在nil映射上执行的,那就是存储操作。如果我们试图向nil映射中添加任何键值对,程序会立即崩溃并抛出panic错误:
|// ages["小明"] = 21 // panic: 向nil映射中赋值
通过下标访问映射元素总是产生一个值。如果键存在于映射中,你得到相应的值; 如果不存在,你得到元素类型的零值。对于许多目的来说这很好,但有时你需要知道元素是否真的存在。例如,如果元素类型是数值,你可能需要区分不存在的元素和恰好值为零的元素:
|age, ok := ages["小明"] if !ok { fmt.Println("小明不在这个映射中;age == 0") }
你经常会看到这两个语句结合使用:
|if age, ok := ages["小明"]; !ok { fmt.Println("小明不存在") }
在这种上下文中,映射下标操作产生两个值;第二个是布尔值,报告元素是否存在。布尔变量通常叫做ok,特别是当它立即用于if条件时。
与切片一样,映射不能相互比较;唯一合法的比较是与nil的比较。要测试两个映射是否包含相同的键和相同的关联值,我们必须编写循环:
|func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false }
注意我们如何使用
!ok来区分"缺失"和"存在但为零"的情况。
Go没有提供集合类型,但由于映射的键是不同的,映射可以用于这个目的。下面的程序读取一系列行并只打印每个不同行的第一次出现:
|func main() { seen := make(map[string]bool) // 字符串集合 input := bufio.NewScanner(os.Stdin) for input.Scan() { line := input.Text() if !seen[line] { seen[line] = true fmt.Println(line) } }
有时我们需要一个键为切片的映射或集合,但由于映射的键必须可比较,这不能直接表达。然而,可以分两步完成。首先我们定义一个辅助函数k,它将每个键映射为字符串,具有这样的属性: 当且仅当我们认为x和y等价时,k(x) == k(y)。然后我们创建一个键为字符串的映射,在访问映射之前将辅助函数应用于每个键。
下面的例子使用映射记录Add函数被给定字符串列表调用的次数。它使用fmt.Sprintf将字符串切片转换为适合作为映射键的单个字符串,用%q引用每个切片元素以忠实记录字符串边界:
|var m = make(map[string]int) func k(list []string) string { return fmt.Sprintf("%q", list) } func Add(list []string) { m[k(list)]++ }
11. 数组声明和初始化练习
编写程序,演示数组的不同声明和初始化方式。
|package main import "fmt" func main() { // 方式1:声明并指定长度 var arr1 [5]int arr1[0] = 1 arr1[1] = 2 fmt.Println("arr1:", arr1) // 方式2:声明并初始化 var arr2 [5]int =
12. 切片基本操作练习
编写程序,演示切片的创建、追加和切片操作。
|package main import "fmt" func main() { // 方式1:使用make创建切片 slice1 := make([]int, 3, 5) // 长度3,容量5 slice1[0] = 1 slice1[1] = 2 slice1[2] = 3 fmt.Printf("slice1:
13. 映射基本操作练习
编写程序,演示映射的创建、访问、修改和删除操作。
|package main import "fmt" func main() { // 方式1:使用make创建映射 ages1 := make(map[string]int) ages1["张三"] = 25 ages1["李四"] = 30 fmt.Println("ages1:", ages1) // 方式2:直接初始化 ages2
14. 数组操作练习
编写函数,计算数组中的最大值、最小值和平均值。
|package main import "fmt" func analyzeArray(arr [5]int) (max, min int, avg float64) { if len(arr) == 0 { return 0, 0, 0 } max = arr[
15. 切片过滤练习
编写函数,从字符串切片中过滤出所有非空字符串。
|package main import "fmt" func filterNonEmpty(strs []string) []string { result := []string{} // 创建空切片 for _, s := range strs { if s != "" { // 检查是否非空 result = append(result, s) }
16. 字符统计练习
编写函数,统计字符串中每个字符出现的次数。
|package main import "fmt" func countCharacters(s string) map[rune]int { counts := make(map[rune]int) // 遍历字符串的每个字符(rune) for _, char := range s { counts[char]++ // 如果键不存在,会返回零值0,然后++ }
输出结果:
|arr1: [1 2 0 0 0] arr2: [1 2 3 4 5] arr3: [1 2 3 4 5] arr4: [1 2 3 4 5] arr4长度: 5 arr5: [0 10 0 30 0]
说明:
[5]int和[3]int是不同的类型[...]可以让编译器自动推断数组长度输出结果:
|slice1: [1 2 3], 长度: 3, 容量: 5 slice2: [1 2 3 4 5] slice3: [2 3 4] slice4追加后: [1 2 3 4 5 6] slice5[2:5]: [2 3 4] slice5[:3]: [0 1 2] slice5[7:]: [7 8 9]
说明:
make([]int, length, capacity)创建指定长度和容量的切片[low:high]包含从low到high-1的元素append()用于向切片追加元素,返回新的切片输出结果:
|ages1: map[张三:25 李四:30] ages2: map[王五:28 赵六:32] 张三的年龄: 25 王五的年龄: 0 王五不在映射中 修改后ages1: map[张三:26 李四:30] 添加后ages1: map[张三:26 李四:30 王五:28] 删除后ages1: map[李四:30 王五:28] 遍历映射: 李四: 30岁 王五: 28岁
说明:
make(map[keyType]valueType)创建空映射value, exists := map[key]可以检查键是否存在delete(map, key)用于删除映射中的键值对for range可以遍历映射输出结果:
|数组: [10 25 5 30 15] 最大值: 30 最小值: 5 平均值: 17.00
说明:
for range循环输出结果:
|原切片: [apple banana orange grape ] 过滤后: [apple banana orange grape]
说明:
[]string{}append()向切片追加元素!= ""检查是否非空输出结果:
|字符串: hello world 字符统计: 'h': 1 'e': 1 'l': 3 'o': 2 ' ': 1 'w': 1 'r': 1 'd': 1
说明:
map[rune]int存储字符到计数的映射for range返回的是rune(Unicode字符)++递增