11 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-05-14 15:30 |
Go make() 函数备忘
为什么需要 make?
在 Go 中,声明一个切片、映射或通道时,它们的内部结构处于零值状态——就像一个空纸箱还没装入任何物品。make 的作用就是给这三种类型分配并初始化底层数据结构,让后续的使用成为可能。
[!question] 思考 假设你有一个切片
var s []int,它看起来"存在"了,但长度为 0、容量为 0。此时如果直接往里面写数据会发生什么?(提示:试试s[0] = 1)答案: panic: runtime error: index out of range。因为底层数组根本没有分配空间。
核心概念:长度 vs 容量
理解 make 之前,必须先区分两个概念:
| 概念 | 含义 | 类比 |
|---|---|---|
| 长度 (len) | 当前已使用的元素个数,可遍历的范围 | 盒子里已放了多少个球 |
| 容量 (cap) | 底层数组的最大承载量,追加的上限 | 盒子总共能装多少个球 |
flowchart LR
A["make([]int, 3, 5)"] --> B["底层数组: 5 个 int 槽位"]
B --> C["索引 0~2: 可见区(len=3)"]
B --> D["索引 3~4: 隐藏区(cap-len=2)"]
C --> E["append 可直接用"]
D --> E
D --> F["超出 cap 时会触发扩容"]
[!tip] 直观公式
cap - len= 还能不扩容的情况下 append 多少次扩容触发条件 ⚡
当
len == cap时,下一次append会触发扩容。Go 的策略是:容量 ≤ 1024 时翻倍,超过 1024 时增长约 25%。这是为了兼顾小切片的性能和大规模增长的渐进成本。
语法速查
make(Type, size[, capacity])
| 参数位置 | 必选? | 说明 |
|---|---|---|
| Type | ✅ | 只能是 slice、map、channel 三种之一 |
| size | ✅ (slice/channel) | 初始长度或初始容量 |
| capacity | ❌ | 仅 slice 支持,省略时等于 size |
三种类型的调用方式
// ── Slice ───────────────────────────────────────
s := make([]int, 3) // len=3, cap=3 → [0, 0, 0]
s := make([]int, 3, 5) // len=3, cap=5 → [0, 0, 0] + 2 个预留位
// ── Map ─────────────────────────────────────────
m := make(map[string]int) // 空映射,size 参数被忽略
m := make(map[string]int, 10) // 预分配约 10 个 bucket(非精确值)
// ── Channel ─────────────────────────────────────
ch := make(chan int) // 无缓冲信道(同步)
ch := make(chan int, 0) // 同上,显式写 0
ch := make(chan int, 3) // 缓冲信道,buffer 大小 = 3
[!important] 关键点 map 的第二个参数是 "hint",不是精确大小。Go 会根据这个提示预分配 bucket,但最终大小由运行时以 2 的幂次向上取整决定。设置合理 hint 可以避免频繁哈希 rehash 带来的性能开销。
深入理解:三种类型在内存中的样子
1. Slice —— 三个字段的结构体
make([]int, 3, 5)
flowchart LR
subgraph SH["slice header(3 个字段)"]
P["pointer → 底层数组"]
LN["len = 3"]
CP["cap = 5"]
end
subgraph UA["底层数组(容量 5)"]
V1["✓ 索引 0"]
V2["✓ 索引 1"]
V3["✓ 索引 2"]
H1["隐藏 索引 3"]
H2["隐藏 索引 4"]
end
LN -- "可见区" --> V1
LN -- "..." --> V2
LN -- "..." --> V3
CP -. "隐藏区" .-> H1
CP -. "..." .-> H2
SH --- UA
style SH fill:#e3f2fd,stroke:#90caf9,color:#333
style UA fill:#e8f5e9,stroke:#a5d6a7,color:#333
style H1 fill:#f5f5f5,stroke:#bdbdbd,color:#999
style H2 fill:#f5f5f5,stroke:#bdbdbd,color:#999
[!example] 验证代码
s := make([]int, 3, 5) fmt.Println(len(s)) // 3 fmt.Println(cap(s)) // 5
2. Map —— 哈希表的 bucket 组织
make(map[string]int, 8)
flowchart TD
A["hmap struct"] --> B["buckets: 指向 bucket 数组的指针"]
A --> C["oldbuckets: 旧桶(迁移时使用)"]
A --> D["nBuckets: 当前桶数量"]
A --> E["count: 当前元素总数"]
B --> F["bucket 数组: 8+ 个 bucket"]
F --> G["bucket 0: 8 对 key/value + overflow 指针"]
F --> H["bucket 1: ..."]
F --> I["..."]
style A fill:#e1f5fe
style G fill:#fff3e0
每个 bucket 包含:
- 8 个 key 的 hash 高 8 位(topHash)
- 8 个 key 值
- 8 个 value 值
- 一个 overflow 指针(指向下一个 bucket)
[!warning] 误区提醒
make(map[K]V, n)中的n只建议当你知道大致键数量时使用。如果不确定,直接make(map[K]V)就够了——Go 会自动按需分配。
3. Channel —— 环形缓冲区 + 锁
ch := make(chan int, 3)
flowchart LR
subgraph ChanStruct["chan struct(运行时内部表示)"]
BUF["buf\\n[int, int, int]\\nbuffer 槽位"]
SQ["sendQ\\ngoroutine 等待队列"]
RQ["recvQ\\ngoroutine 等待队列"]
MX["mutex\\nsync.Mutex"]
end
ChanStruct --> BUF
ChanStruct --> SQ
ChanStruct --> RQ
ChanStruct --> MX
style ChanStruct fill:#e3f2fd,stroke:#90caf9,color:#333
style BUF fill:#fff3e0,stroke:#ffcc80,color:#333
style SQ fill:#f3e5f5,stroke:#ce93d8,color:#333
style RQ fill:#f3e5f5,stroke:#ce93d8,color:#333
style MX fill:#fce4ec,stroke:#f48fb1,color:#333
- 无缓冲 (buf=0): 发送和接收必须配对执行,一方等待另一方(同步)
- 有缓冲 (buf=N): 发送端写入 buffer 后立即返回,直到 buffer 满才阻塞
[!tip] channel 的方向限定词
make只能创建双向 channel,但可以通过类型转换将其"降级"为单向:ch := make(chan int, 3) // 双向 channel sendCh := (func(chan int)(ch)) // → chan<- int,只能发送 recvCh := (<-chan int)(ch) // → <-chan int,只能接收这在函数参数中非常有用——声明为
func worker(ch chan<- int)可以明确表达该函数只往 channel 发消息。
常见用法场景
场景 1:预分配切片减少 GC 压力
// ❌ 不推荐:反复扩容,每次都要分配新数组 + 拷贝旧数据
s := []int{}
for i := 0; i < 10000; i++ {
s = append(s, i)
}
// ✅ 推荐:一次性分配好容量
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
[!tip] 技巧
make([]T, 0, N)是经典模式:长度为 0(从第一个 append 开始),容量为 N(不再扩容)。配合append使用,既避免了越界 panic,又消除了重复分配。
场景 2:Map 预分配避免 Rehash
// 已知大约有 1000 个键
m := make(map[string]*User, 1000)
提前告知 Go 的大致规模,可以减少哈希表扩容时的重新散列成本。但如果实际数据远超预期,运行时仍会扩容——所以不必追求绝对精确。
场景 3:Channel 用于 Goroutine 通信
// 工作池模式
jobs := make(chan int, 100)
results := make(chan result, 100)
// 启动 worker
for w := 1; w <= 5; w++ {
go worker(jobs, results)
}
// 关闭 jobs 后,worker 会自然结束
close(jobs)
[!question] 思考 为什么这里两个 channel 都使用了缓冲?如果没有缓冲会发生什么问题?
思路: 无缓冲时 sender 和 receiver 必须同时准备,这会严重限制并发效率,变成严格的同步调用。缓冲让两者可以解耦,workers 可以继续处理而不被下游阻塞。
场景 4:与 new() 的区别
// new(T):分配零值内存,返回 *T
p := new(int) // p = (*int)(0xc000010200), *p == 0
// make(T):仅适用于 slice/map/channel,返回 T 本身
s := make([]int, 3) // s 是 []int,不是 *[]int
| 特性 | new(T) | make(T) |
|---|---|---|
| 适用类型 | 所有类型 | 仅 slice / map / channel |
| 返回值 | *T |
T |
| 行为 | 分配零值内存 | 初始化内部数据结构 |
| 能否用于 struct | ✅ | ❌ |
[!tip] 经验法则 如果你的类型需要"内部初始化"(如 slice 的底层数组、map 的 bucket、channel 的 buffer),只能用
make;如果只是分配一块零值内存,用new。对于普通 struct,大多数时候直接用字面量MyStruct{}就行。
决策流程图
flowchart TD
Start["需要一个变量"] --> Q1{"类型是 slice/map/channel?"}
Q1 -->|否| NewOrLit["用 new() 或 字面量 {}"]
Q1 -->|是| WhichType{"哪种类型?"}
WhichType -->|Slice| SliceQ{"知道最终长度?"}
SliceQ -->|知道| SlicePre["make([]T, 0, knownLen)"]
SliceQ -->|不知道| Append["直接字面量 []T{} 或 make([]T, n)"]
WhichType -->|Map| MapHint{"知道大致键数?"}
MapHint -->|知道| MapWithHint["make(map[K]V, hint)"]
MapHint -->|不知道| MapNoHint["make(map[K]V)"]
WhichType -->|Channel| ChanQ{"需要同步还是缓冲?"}
ChanQ -->|同步| UnbuffChan["make(chan T)"]
ChanQ -->|缓冲| BuffChan["make(chan T, bufSize)"]
SlicePre --> End["完成"]
Append --> End
MapWithHint --> End
MapNoHint --> End
UnbuffChan --> End
BuffChan --> End
NewOrLit --> End
避坑指南
Pitfall 1:切片共享底层数组
a := make([]int, 4) // [0, 0, 0, 0]
b := a[:3] // [0, 0, 0],cap=4,共享底层数组!
b[0] = 99
fmt.Println(a) // [99, 0, 0, 0] ← a 也被改变了!
[!danger] 危险操作 使用切片截取时,新切片可能共享原切片的底层数组。修改其中一个会影响另一个。如果需要隔离,使用
copy()创建一个独立副本。
Pitfall 2:nil map vs empty map
var m map[string]int // nil map,读写都会 panic
m2 := make(map[string]int) // empty map,安全可用
m2["key"] = 1 // ✅ 正常
m["key"] = 1 // ❌ panic: assignment to entry in nil map
[!tip] 养成习惯 声明 map 时用
:= make(...)而不是var m map[K]V,除非你有明确的延迟初始化意图。
Pitfall 3:误把 map 的 hint 当精确值
m := make(map[string]int, 10)
// 实际分配的 bucket 数量 ≈ 16(2^4),因为 Go 按 2 的幂次分配
[!warning] 记住这一点
make里的数字只是估算提示。Go 运行时为了哈希性能采用 2 的幂次分配 bucket,所以实际容量可能比预期大。不必纠结精确值——合理范围内即可。
总结速查卡
| 类型 | 最小写法 | 带容量写法 | 零值是否可用 |
|---|---|---|---|
[]int |
make([]int, n) |
make([]int, n, c) |
❌ 需 make |
map[K]V |
make(map[K]V) |
make(map[K]V, n) |
❌ 需 make |
chan T |
make(chan T) |
make(chan T, n) |
❌ 需 make |