这篇笔记梳理 Go 语言三种内置容器的核心用法:数组切片映射。每种都覆盖定义、增删改查、遍历和常见坑点。


📦 一、数组(Array)

关键词: 固定长度 · 值类型 · 编译期确定大小

Go 中数组的长度是类型的一部分 —— [3]int[4]int 是两种不同的类型。

1.1 四种初始化方式

1
2
3
4
5
6
7
8
9
10
11
12
13
// ① 声明后赋值 — 默认零值
var a [3]int // [0, 0, 0]
a[0] = 1

// ② 声明时指定元素
b := [3]int{1, 2, 3}

// ③ ... 推导长度 — 改元素个数不用改长度
c := [...]int{1, 2, 3, 4, 5}

// ④ 指定索引初始化 — 未指定的为零值
d := [5]int{0: 1, 3: 4}
// 结果: [1, 0, 0, 4, 0]

1.2 访问与修改

1
2
3
4
5
6
7
arr := [5]int{10, 20, 30, 40, 50}

fmt.Println(arr[0]) // 10 ← 访问
fmt.Println(arr[4]) // 50

arr[2] = 99 ← 修改
fmt.Println(arr) // [10 20 99 40 50]

⚠️ 索引越界:常量索引编译期报错,变量索引运行期 panic。

1.3 遍历

1
2
3
4
5
6
7
8
9
10
11
12
arr := [4]string{"Go", "Java", "Python", "Rust"}

// ✅ 推荐 — for-range 同时拿到索引和值
for i, v := range arr {
fmt.Printf("[%d] %s\n", i, v)
}

// 只要索引
for i := range arr { ... }

// 只要值(用 _ 忽略索引)
for _, v := range arr { ... }

1.4 多维数组

1
2
3
4
5
6
7
8
9
10
11
// 二维数组:2 行 3 列
var matrix [2][3]int
matrix[0] = [3]int{1, 2, 3}
matrix[1] = [3]int{4, 5, 6}

// 遍历
for i := range matrix {
for j := range matrix[i] {
fmt.Printf("[%d][%d]=%d ", i, j, matrix[i][j])
}
}

1.5 ⚠️ 数组是值类型

赋值和传参时完整拷贝整个数组:

1
2
3
4
5
6
a := [3]int{1, 2, 3}
b := a // 拷贝了一份
b[0] = 99

fmt.Println(a) // [1 2 3] ← 不受影响
fmt.Println(b) // [99 2 3]

踩坑: 大数组传参会拷贝整个数组,开销很大 → 改用切片


✂️ 二、切片(Slice)

关键词: 动态长度 · 引用类型 · 最常用容器

切片不存储数据,而是指向底层数组的一段连续区域。

2.1 四种初始化方式

1
2
3
4
5
6
7
8
9
10
11
12
13
// ① make — 指定长度和容量
s1 := make([]int, 5) // [0 0 0 0 0] len=5, cap=5
s2 := make([]int, 3, 10) // [0 0 0] len=3, cap=10

// ② 字面量
s3 := []int{1, 2, 3, 4, 5}

// ③ 从数组截取 — 左闭右开
arr := [5]int{10, 20, 30, 40, 50}
s4 := arr[1:4] // [20, 30, 40]

// ④ nil 切片
var s5 []int // nil, len=0, cap=0

2.2 len vs cap

含义示例
len当前有多少个元素len([0,0,0]) = 3
cap不触发扩容最多能装多少从切片起点到底层数组末尾
1
2
3
s := make([]int, 3, 10)
// len=3 ← 现在有 3 个元素
// cap=10 ← 最多能装 10 个

2.3 追加元素 — append

1
2
3
4
5
s := []int{1, 2, 3}

s = append(s, 4) // 追加一个 → [1 2 3 4]
s = append(s, 5, 6, 7) // 追加多个 → [1 2 3 4 5 6 7]
s = append(s, other...) // 追加另一个切片

⚠️ 关键:必须把返回值赋回原变量 s = append(s, ...),append 可能返回新地址。

2.4 扩容机制

len == cap 时再 append,Go 会分配更大的底层数组(通常翻倍):

1
2
3
4
5
s := make([]int, 0, 2)

s = append(s, 1) // len=1, cap=2
s = append(s, 2) // len=2, cap=2
s = append(s, 3) // len=3, cap=4 ← 翻倍扩容!

2.5 删除元素

Go 没有内置删除函数,用切片拼接实现:

1
2
3
4
5
6
7
8
9
10
s := []int{1, 2, 3, 4, 5}

// 删除索引 2 的元素(值 3)
s = append(s[:2], s[3:]...) // [1 2 4 5]

// 删除第一个
s = s[1:] // [2 4 5]

// 删除最后一个
s = s[:len(s)-1] // [2 4]

2.6 遍历

1
2
3
4
5
s := []string{"Go", "Java", "Python"}

for i, v := range s {
fmt.Printf("[%d] %s\n", i, v)
}

2.7 copy — 安全复制

1
2
3
4
5
src := []int{1, 2, 3, 4, 5}

dst := make([]int, len(src))
copy(dst, src)
// dst 和 src 完全独立

2.8 ⚠️ 切片是引用类型

共享底层数组,修改一个会影响另一个:

1
2
3
4
5
a := []int{1, 2, 3}
b := a
b[0] = 99

fmt.Println(a) // [99 2 3] ← 被改了!
1
2
3
4
5
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2, 3, 4]
s[0] = 99

fmt.Println(arr) // [1, 99, 3, 4, 5] ← 原数组也被改了!

想要独立副本?用 copy:

1
2
3
4
5
6
s := arr[1:4]
indep := make([]int, len(s))
copy(indep, s)
indep[0] = 99

fmt.Println(arr) // [1, 2, 3, 4, 5] ← 安全

2.9 nil 切片 vs 空切片

nil 切片空切片
写法var s []int[]int{}make([]int, 0)
== niltruefalse
JSON 序列化null[]
能否 range
能否 append

💡 一般用 var s []int 就行,append 时 Go 会自动分配底层数组。


🗂️ 三、映射(Map)

关键词: 键值对 · 哈希表 · O(1) 查找

键必须是可比较的类型(切片、map、函数不能作 key)。

3.1 三种初始化方式

1
2
3
4
5
6
7
8
9
10
11
12
// ① make
m1 := make(map[string]int)

// ② 字面量
m2 := map[string]int{
"Go": 1,
"Java": 2,
"Python": 3,
}

// ③ nil map
var m3 map[string]int // nil

3.2 添加与修改

1
2
3
4
5
6
m := make(map[string]int)

m["Go"] = 1 // 添加
m["Java"] = 2

m["Go"] = 99 // 修改(语法和添加一样)

⚠️ nil map 写入会 panic! 必须先 make

1
2
3
4
5
var m map[string]int
m["key"] = 1 // 💥 panic!

m = make(map[string]int)
m["key"] = 1 // ✅

3.3 查找 — comma-ok 惯用法

1
2
3
4
5
6
7
8
9
10
11
12
13
m := map[string]int{"Go": 1, "Java": 2}

// ❌ 直接取值 — key 不存在时返回零值,无法区分
v := m["Rust"] // 0(是存的 0 还是不存在?)

// ✅ comma-ok — 第二个返回值表示是否存在
if val, ok := m["Go"]; ok {
fmt.Println("找到:", val)
}

if _, ok := m["Rust"]; !ok {
fmt.Println("不存在")
}

3.4 删除

1
2
3
4
m := map[string]int{"Go": 1, "Java": 2, "Python": 3}

delete(m, "Java") // 删除
delete(m, "Rust") // 删除不存在的 key 也不报错

3.5 遍历(无序!)

1
2
3
4
5
m := map[string]int{"Go": 1, "Java": 2, "Python": 3}

for key, val := range m {
fmt.Printf("%s => %d\n", key, val)
}

⚠️ map 遍历是随机的! 每次运行顺序可能不同。

需要有序遍历?先排序 key:

1
2
3
4
5
6
7
8
9
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
fmt.Printf("%s => %d\n", k, m[k])
}

3.6 map 是引用类型

1
2
3
4
5
m1 := map[string]int{"Go": 1}
m2 := m1
m2["Go"] = 99

fmt.Println(m1) // map[Go:99] ← 共享底层哈希表

3.7 key 类型限制

✅ 可以❌ 不可以
int, string, bool切片 []int
数组 [2]intmap map[string]int
指针函数 func()

3.8 操作速查

1
2
3
4
5
6
7
m := make(map[string]int)

m["key"] = 1 // 添加 / 修改
val, ok := m["key"] // 查找
delete(m, "key") // 删除
len(m) // 长度
clear(m) // 清空(Go 1.21+)

⚡ 四、三者对比速查

特性数组切片映射
长度固定动态动态
类型值类型引用类型引用类型
初始化[n]T{...}[]T{...} / makemap[K]V{...} / make
追加appendm[k] = v
删除切片拼接delete(m, k)
查找遍历 O(n)遍历 O(n)m[k] O(1)
遍历for-rangefor-rangefor-range(无序)

🎯 五、实战小贴士

  1. 优先用切片,少用数组 — 除非长度固定(如 [3]byte 表示 RGB)
  2. 预分配容量 — 知道大概存多少,用 make([]T, 0, cap) 避免频繁扩容
  3. map 查找用 comma-ok — 别靠零值判断 key 是否存在
  4. 遍历 map 要有序? 先取 key 排序
  5. 切片截取共享底层数组 — 要独立副本用 copy
  6. nil map 写入会 panic — 必须先 make