Go 的集合与字符串
Go 使用 Array 和 Slice 表示一组相同类型的值,使用 Map 保存键值数据,使用 String 处理只读文本。它们的语法并不复杂,但在复制、共享存储和零值行为上各有不同。
本文从固定长度的 Array 开始,依次介绍 Slice 的长度与容量、Map 的读写方式,以及 String、byte 和 rune 之间的关系。
Array
Array 是长度固定的同类型元素序列。声明数组时,长度是类型的一部分:
numbers := [3]int{10, 20, 30}
fmt.Println(numbers[0]) // 10
fmt.Println(len(numbers)) // 3[3]int 和 [4]int 是两种不同的类型。数组创建后不能改变长度,因此更适合元素数量在编译时已经确定的场景。
数组也是值类型。把一个数组赋给另一个变量时,Go 会复制其中的全部元素,修改副本不会影响原数组:
original := [3]int{10, 20, 30}
copied := original
copied[0] = 100
fmt.Println(original) // [10 20 30]
fmt.Println(copied) // [100 20 30]Slice
实际开发中,元素数量经常需要动态变化,这时通常使用 Slice。Slice 的字面量与数组相似,但类型中不包含固定长度:
numbers := []int{10, 20, 30}
numbers = append(numbers, 40)
fmt.Println(numbers) // [10 20 30 40]Slice 并不直接保存全部元素,它描述的是底层数组中的一段连续区域,主要包含长度和容量两个信息:
- 长度是当前 Slice 中可以访问的元素数量,通过
len获取; - 容量是从 Slice 起点到底层数组末尾可以容纳的元素数量,通过
cap获取。
numbers := make([]int, 2, 4)
numbers[0] = 10
numbers[1] = 20
fmt.Println(len(numbers)) // 2
fmt.Println(cap(numbers)) // 4这里的 make([]int, 2, 4) 创建了一个长度为 2、容量为 4 的 Slice。前两个元素已经存在,并获得 int 的零值;剩余容量暂时不能直接通过下标访问。
截取 Slice
切片表达式使用左闭右开的区间。numbers[1:3] 包含索引 1 和 2,不包含索引 3:
numbers := []int{10, 20, 30, 40}
middle := numbers[1:3]
fmt.Println(middle) // [20 30]截取得到的新 Slice 通常与原 Slice 共享底层数组,因此通过其中一个 Slice 修改元素,另一个也能看到变化:
numbers := []int{10, 20, 30, 40}
middle := numbers[1:3]
middle[0] = 200
fmt.Println(numbers) // [10 200 30 40]NOTE
Slice 赋值和截取复制的是 Slice 的描述信息,而不是底层元素。需要互不影响的数据时,应显式复制元素。
使用 append 追加元素
append 会返回追加后的 Slice,因此必须接收它的返回值:
numbers := []int{10, 20}
numbers = append(numbers, 30, 40)如果原 Slice 的剩余容量足够,追加的元素可以继续写入现有底层数组;如果容量不足,运行时会分配一个更大的底层数组并复制已有元素。因此,不能假设 append 前后的 Slice 始终共享同一个底层数组。
使用 make 和 copy
当长度或容量需要在运行时确定时,可以使用 make 创建 Slice:
empty := make([]int, 0, 4)
ready := make([]int, 3)
fmt.Println(len(empty), cap(empty)) // 0 4
fmt.Println(ready) // [0 0 0]第一个参数是 Slice 类型,第二个参数是长度,可选的第三个参数是容量。容量不能小于长度。
需要创建独立副本时,先创建目标 Slice,再使用 copy 复制元素:
numbers := []int{10, 20, 30}
cloned := make([]int, len(numbers))
copy(cloned, numbers)
cloned[0] = 100
fmt.Println(numbers) // [10 20 30]
fmt.Println(cloned) // [100 20 30]Map
Map 用于保存键值对。下面的 Map 使用 string 作为键,使用 int 作为值:
scores := map[string]int{
"Alice": 90,
"Bob": 85,
}
scores["Carol"] = 92
fmt.Println(scores["Alice"]) // 90也可以使用 make 创建空 Map,再逐项写入数据:
scores := make(map[string]int)
scores["Alice"] = 90读取一个不存在的键不会报错,而是返回值类型的零值。需要区分“键不存在”和“值恰好为零”时,应接收第二个布尔返回值:
score, exists := scores["Carol"]
if !exists {
fmt.Println("score not found")
} else {
fmt.Println(score)
}使用 delete 删除键。删除不存在的键也是安全的:
delete(scores, "Bob")遍历 Map 时可以使用 range,但遍历顺序没有保证。如果输出顺序有要求,需要在业务代码中另行排序。
for name, score := range scores {
fmt.Println(name, score)
}nil Slice 与 nil Map
Slice 和 Map 的零值都是 nil,但两者的行为并不相同。
nil Slice 的长度和容量都是 0,可以读取、遍历,也可以直接使用 append:
var numbers []int
fmt.Println(len(numbers), cap(numbers)) // 0 0
numbers = append(numbers, 10)
fmt.Println(numbers) // [10]nil Map 可以读取,结果是值类型的零值,但不能写入:
var scores map[string]int
fmt.Println(scores["Alice"]) // 0
// scores["Alice"] = 90 // panic: assignment to entry in nil map需要写入 Map 时,应先通过字面量或 make 完成初始化。
String、byte 与 rune
Go 的 String 是不可变的只读字节序列,通常用 UTF-8 编码保存文本。len 返回字符串占用的字节数,而不是人眼看到的字符数量:
text := "Go语言"
fmt.Println(len(text)) // 8
fmt.Println(text[0]) // 71通过下标读取字符串时,得到的是一个 byte。byte 是 uint8 的别名,适合表示原始字节。
需要按 Unicode 码点遍历文本时,可以使用 range。每次循环得到的字符是 rune,索引则是该字符在字符串中的起始字节位置:
text := "Go语言"
for index, char := range text {
fmt.Printf("%d: %c\n", index, char)
}rune 是 int32 的别名,用于表示 Unicode 码点。上面的索引依次为 0、1、2 和 5,因为“语”和“言”在 UTF-8 中各占三个字节。
字符串不能直接修改。处理原始字节时可以转换为 []byte,按 Unicode 码点修改文本时可以转换为 []rune,完成后再转回 String:
text := "Go语言"
chars := []rune(text)
chars[2] = '编'
text = string(chars)
fmt.Println(text) // Go编言总结
Array 的长度固定,赋值时会复制全部元素;Slice 通过底层数组提供可变长度的序列,并使用 len、cap、append 和 copy 管理可见范围与数据副本。Map 适合保存键值数据,读取时要根据需要检查键是否存在。
处理零值时,需要记住 nil Slice 可以直接追加,而 nil Map 必须初始化后才能写入。处理字符串时,则应先分清操作目标是原始字节还是 Unicode 码点,再选择 byte 或 rune。