主题
第05章 集合类型 —— 数组、切片、map、字符串
本章目标:搞清楚 Go 中最容易踩坑的部分:切片底层共享、map 的零值规则、字符串与 rune 的关系。
1. 数组(Array):固定长度
1.1 声明
go
var a [5]int // [0 0 0 0 0]
b := [3]string{"Go", "Rust", "C"}
c := [...]int{1, 2, 3, 4} // 编译器自动推断长度 = 41.2 数组特点
- 长度是类型的一部分!
[3]int和[5]int是不同类型,不能赋值 - 数组是值类型:拷贝整个数组,函数传参也是拷贝
🌰 生活化比喻:数组像固定 5 格的药盒,每天吃一格,盒子格数定死了,不能拆。
go
func mod(arr [3]int) { arr[0] = 100 }
a := [3]int{1, 2, 3}
mod(a)
fmt.Println(a) // [1 2 3] 不变!📊 配套图解:diagrams/array-vs-slice.svg
💡 实际开发中几乎不用数组,都用切片。
2. 切片(Slice):Go 的灵魂
2.1 切片是什么?
切片是对底层数组的视图,由三部分组成:
┌─────────────┐
│ ptr → [底层数组]
│ len = 当前长度
│ cap = 底层数组容量
└─────────────┘🌰 生活化比喻:切片像电视机的画面缩放——底层数组是整个电视屏幕,切片是你"圈定"的一块区域,可以放大缩小,但实际内容还在屏幕上。
2.2 创建切片的几种方式
go
s1 := []int{1, 2, 3} // 字面量
s2 := make([]int, 5) // 长度 5,容量 5
s3 := make([]int, 5, 10) // 长度 5,容量 10
s4 := []int(nil) // nil 切片
s5 := []int{} // 空切片(len=0, cap=0,但非 nil)2.3 append 的扩容机制
go
s := []int{1, 2, 3}
s = append(s, 4)
s = append(s, 5, 6, 7)扩容规则(Go 1.18+):
- 容量 < 256:翻倍
- 容量 ≥ 256:每次 +25%(缓增)
📊 配套图解:diagrams/slice-internals.svg
2.4 ⚠️ 切片的"共享"陷阱
go
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2, 3]
b[0] = 99
fmt.Println(a) // [1 99 3 4 5] a 也被改了!原因:b 和 a 共享同一个底层数组。
如果想真正复制,用 copy 或 append:
go
b := append([]int(nil), a[1:3]...) // 真复制
// 或
b := make([]int, 2)
copy(b, a[1:3])📊 配套图解:diagrams/slice-share.svg
2.5 切片操作速查
| 操作 | 写法 |
|---|---|
| 添加 | s = append(s, x) |
| 删除中间 | s = append(s[:i], s[i+1:]...) |
| 删除末尾 | s = s[:len(s)-1] |
| 复制 | dst := make([]int, len(src)); copy(dst, src) |
| 反转 | slices.Reverse(s) (Go 1.21+) |
| 排序 | sort.Ints(s) 或 slices.Sort(s) |
3. Map:键值对字典
3.1 创建与使用
go
m := make(map[string]int)
m["Alice"] = 90
m["Bob"] = 85
// 字面量
scores := map[string]int{
"Alice": 90,
"Bob": 85,
}3.2 关键操作
go
// 读取(重要:双返回值检测 key 是否存在)
v, ok := m["Alice"]
if ok { fmt.Println("找到", v) }
// 删除
delete(m, "Alice")
// 长度
len(m)
// 遍历(顺序随机!)
for k, v := range m {
fmt.Println(k, v)
}3.3 ⚠️ map 的坑
坑1:nil map 不能写
go
var m map[string]int // nil map
m["a"] = 1 // ❌ panic!
m = make(map[string]int)
m["a"] = 1 // ✅🌰 生活化比喻:你拿到一个空地址的钥匙——根本没有房子,怎么往里塞东西?
坑2:map 是"引用"类型
go
m1 := map[string]int{"a": 1}
m2 := m1
m2["b"] = 2
fmt.Println(m1) // map[a:1 b:2] m1 也被改了坑3:遍历顺序随机
go
for k, v := range m {
// 顺序每次都不同!这是 Go 故意的,防止你依赖顺序
}要有序遍历,先把 key 排序:
go
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}坑4:并发不安全
map 不是并发安全的。多 goroutine 读写要用 sync.RWMutex 或 sync.Map(第09章细讲)。
📊 配套图解:diagrams/map-internals.svg
4. 字符串:高级版字节切片
4.1 字符串本质
go
s := "Hello, 世界"
// 底层:{ data *byte; len int }
// data 指向只读内存4.2 字符串常用操作
go
import "strings"
strings.ToUpper("hello") // "HELLO"
strings.Contains("hello", "ll") // true
strings.Split("a,b,c", ",") // ["a", "b", "c"]
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.Replace("hello", "l", "L", 1) // "heLlo"
strings.TrimSpace(" hi ") // "hi"4.3 字符串与 byte/rune 互转
go
s := "Hi你好"
// string -> []byte
b := []byte(s)
// string -> []rune(按字符)
r := []rune(s)
fmt.Println(len(r)) // 4
// []byte -> string
s2 := string(b)4.4 高效拼接:strings.Builder
go
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("hello")
}
result := b.String()⚠️ 不要用 + 拼接大量字符串,每次都会分配新内存。
5. make vs new
make(T, ...) | new(T) | |
|---|---|---|
| 适用类型 | slice, map, chan | 任何类型 |
| 返回 | 类型 T 本身 | *T(指针) |
| 初始化 | 完整初始化 | 仅分配内存(零值) |
go
s := make([]int, 5) // []int 类型,可立即用
m := make(map[string]int) // 可立即用
p := new(int) // *int,*p == 06. 综合案例:词频统计
go
package main
import (
"fmt"
"sort"
"strings"
)
func main() {
text := "go is fun go is simple go runs fast"
words := strings.Fields(text)
freq := make(map[string]int)
for _, w := range words {
freq[w]++
}
type kv struct { key string; val int }
var sorted []kv
for k, v := range freq {
sorted = append(sorted, kv{k, v})
}
sort.Slice(sorted, func(i, j int) bool { return sorted[i].val > sorted[j].val })
for _, kv := range sorted {
fmt.Printf("%s: %d\n", kv.key, kv.val)
}
}输出:
go: 3
is: 2
fun: 1
simple: 1
runs: 1
fast: 17. 本章小结
✅ 数组长度是类型一部分,是值类型——实战中很少用
✅ 切片是指针 + len + cap 的视图,是 Go 最常用的容器
✅ 切片可能因共享底层数组而出现"诡异"修改
✅ map 必须 make 初始化才能写
✅ map 遍历顺序故意随机
✅ 字符串底层是字节序列,索引按字节,遍历 rune 才是按字符
✅ 大量字符串拼接用 strings.Builder