Go:类型、控制流、数组值复制与集合 · 数组与值复制至练习与自检
前置:能在模块目录运行 main.go。本章每个完整程序分别替换上一章 main.go,再 go run .;不要在同一目录保留多个 main 函数。Go 1.27.2,Windows/Linux 运行核对属于历史记录,本轮示例未执行;macOS 仅作平台说明,本项目不执行平台运行验证。
数组与值复制
数组适合固定数量的同类型元素,下标从 0 开始。长度属于类型,[3]int 与 [4]int 不同。先复制数组,再修改副本:
package main
import "fmt"
func main() {
scores := [3]int{1, 2, 3}
copied := scores
copied[0] = 9
fmt.Println(scores, copied)
fmt.Println(len(scores), scores[2])
}预期输出:
[1 2 3] [9 2 3]
3 3数组赋值复制元素,因此原数组不变。合法下标满足 0 <= i && i < len(scores);越界不会自动扩容。省略部分初值的元素为零值。需要增删时用切片,但不能将数组复制的结论直接套到切片。
练习:改成 [3]int{1},再改成 []int{1,2,3},分别预测原值和副本。
参考解法
前者输出 [1 0 0] [9 0 0];后者两者均为 [9 2 3],因为切片共享底层数组,下一节解释该关系。
二维数组也是值
[2][3]int 是包含两个 [3]int 的数组。访问 matrix[row][column],两层下标各有自己的范围;它不是可以任意增删行列的表格对象。下面的独立程序比较原数组与副本。
package main
import "fmt"
func main() {
matrix := [2][3]int{{1, 2, 3}, {4, 5, 6}}
copied := matrix
copied[0][0] = 9
fmt.Println(matrix[0], copied[0])
fmt.Println(len(matrix), len(matrix[0]))
}预期输出:
[1 2 3] [9 2 3]
2 3元素本身仍是数组,赋值会复制全部整数。练习:用两层 range 填写 matrix[row][col] = row*10 + col,预期矩阵为 [[0 1 2] [10 11 12]]。
切片追加:长度、容量与共享底层数组
数组 [3]int 的长度属于类型,赋值复制全部元素;切片 []int 是对底层数组一段区域的描述,包含指针、长度和容量。len 是当前可访问长度,cap 是向后可用的容量。切片赋值只复制描述,不会复制底层元素。函数接收切片后改元素通常可见,但调整长度的结果必须返回或重新赋值。
package main
import "fmt"
func main() {
original := []int{1, 2, 3}
view := original[:2]
view[0] = 9
view = append(view, 8)
fmt.Println(original)
isolated := append([]int(nil), original...)
isolated[0] = 7
fmt.Println(original, isolated)
limited := original[:2:2]
limited = append(limited, 4)
limited[0] = 6
fmt.Println(original, limited)
}预期输出:
[9 2 8]
[9 2 8] [7 2 8]
[9 2 8] [6 2 4]第一处 append 的容量够用,所以覆盖 original 的第三个元素。original... 将切片元素逐个传给 append。复制到新切片后,修改整数元素互不影响;如果元素本身包含指针或切片,这种复制仍是浅拷贝。三下标形式 [:2:2] 把容量限制为 2,因此此次 append 必须分配新底层数组;在 append 之前 两者仍共享。
不能依赖固定扩容倍数判断是否共享;“容量不足时重新分配”才是这里需要的保证。make([]int, 0, 10) 是长度 0、容量 10,不能直接访问 [0],要 append;make([]int, 10) 才已有十个零值元素。只读一小段巨型切片也可能保留整个底层数组,需要独立生命周期时复制所需部分。
切片复制、删除与排序
前置:切片共享。复制到已有空间用 copy,它只复制源、目标长度的较小值,不自动扩容。需要不改变原值的删除或排序,先复制;含指针的元素仍是浅拷贝。
package main
import (
"fmt"
"slices"
)
func main() {
original := []int{3, 1, 2}
edited := slices.Clone(original)
edited = slices.Delete(edited, 1, 2)
slices.Sort(edited)
fmt.Println(original, edited)
short := make([]int, 1)
fmt.Println(copy(short, original), short)
}预期输出:
[3 1 2] [2 3]
1 [3]Delete 的区间是 [i,j),必须满足边界;它修改底层数组并返回新长度,必须接收返回值。当前版本会清零被移出逻辑长度的尾部,减轻尾部指针保留对象的问题;其他共享同一底层数组的切片也会观察到修改,不能继续当独立副本用。slices 包从 Go 1.21 起提供,旧版本行为需另查。
练习:删去 Clone,让 edited 直接引用 original,观察原数组被改动。解释为什么限制 cap 不能隔离已有元素,为什么 copy 到长度 1 的目标只复制一个元素。
参考解法
Clone 为整数元素建立独立存储;赋值只复制切片描述。copy 受 len 而非 cap 限制,想复制全部元素需先创建足够长度的目标。
nil map 写入与键不存在
map 是按键查值的集合,例如 map[string]int。声明 var counts map[string]int 得到 nil map:读键得到零值,写键会触发 assignment to entry in nil map。用 make 或字面量初始化后再写。value, ok := m[key] 区分“没有键”和“键的值恰好为 0”。
package main
import "fmt"
func main() {
counts := make(map[string]int)
for _, name := range []string{"Go", "Go", "SQL"} {
counts[name]++
}
fmt.Println(counts["Go"])
value, ok := counts["Rust"]
fmt.Println(value, ok)
delete(counts, "SQL")
fmt.Println(len(counts))
}预期输出:
2
0 false
1map 遍历顺序不固定,测试不要依赖打印整个 map 的迭代顺序;需要有序业务输出时收集键并排序。map 赋值共享同一份映射;并发读写需要同步,见 并发与测试。
键类型必须可比较,切片不能作为键。map 中保存结构体时,不能直接写 users["a"].Age++,要先取出副本、修改、再赋回。保存指针虽可修改对象,也引入共享状态责任。nil map 的读取、len、range、delete 可用,但写入前仍需初始化。
make(map[string]int, 100) 的第二个参数是容量提示,不是可索引的长度,也不是最大键数;map 没有 cap 操作。是否减少分配需用实际负载测量,不能把 map 容量提示与切片的 len/cap 规则混用。
多数普通集合先用切片或 map。链表、环、堆接口及其选择在标准库容器实践维护,归算法专题,不在这里重复另一份实现。
中文字符串长度与 rune
len("学习Go") 是 8 个 UTF-8 字节,len([]rune("学习Go")) 是 4 个 Unicode 码点。range 字符串返回字节下标与 rune;不要用字节下标随意切开中文。rune 数也不总等于视觉上的字符数,组合音标和表情可能由多个码点构成;本项目标题限制按码点计算。
strings.TrimSpace 去首尾空白,strings.Contains 判断子串。字符串不可原地改字节;需要修改码点时转换为 []rune,修改后再转 string。不要为每个只读操作都转换,普通比较和查找直接使用 strings 包即可。
练习与自检
先预测:a := []int{1,2}; b := a; b[1] = 5 后 a 是什么?然后让 b 的修改不影响 a。给上面的计数程序添加“SQL”一次,预期 SQL 为 2;查询不存在的键时输出“未学习”,而不是把 0 次当成不存在。解释为什么 nil slice 能 append,但 nil map 不能写键。
参考解法
a 变为 [1 5]。用 b := append([]int(nil), a...) 建独立的整数切片;或 make 后 copy。增加输入中的 SQL,再用 count, ok := counts[name],!ok 时打印“未学习”。append 可以返回新切片描述,所以 nil slice 可作为空起点;map 的下标赋值没有返回新 map 的位置,必须先初始化 map。
继续阅读
- 开发实践
- 开发实践
- 开发实践