--- tags: [go, builtin, slice, map, channel] 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)** | 底层数组的最大承载量,追加的上限 | 盒子总共能装多少个球 | ```mermaid 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%**。这是为了兼顾小切片的性能和大规模增长的渐进成本。 ## 语法速查 ```go make(Type, size[, capacity]) ``` | 参数位置 | 必选? | 说明 | |----------|--------|------| | Type | ✅ | 只能是 `slice`、`map`、`channel` 三种之一 | | size | ✅ (slice/channel) | 初始长度或初始容量 | | capacity | ❌ | 仅 slice 支持,省略时等于 size | ### 三种类型的调用方式 ```go // ── 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 —— 三个字段的结构体 ```go make([]int, 3, 5) ``` ```mermaid 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] 验证代码 > ```go > s := make([]int, 3, 5) > fmt.Println(len(s)) // 3 > fmt.Println(cap(s)) // 5 > ``` ### 2. Map —— 哈希表的 bucket 组织 ```go make(map[string]int, 8) ``` ```mermaid 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 —— 环形缓冲区 + 锁 ```go ch := make(chan int, 3) ``` ```mermaid 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,但可以通过**类型转换**将其"降级"为单向: > ```go > 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 压力 ```go // ❌ 不推荐:反复扩容,每次都要分配新数组 + 拷贝旧数据 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 ```go // 已知大约有 1000 个键 m := make(map[string]*User, 1000) ``` 提前告知 Go 的大致规模,可以减少哈希表扩容时的重新散列成本。但如果实际数据远超预期,运行时仍会扩容——所以不必追求绝对精确。 ### 场景 3:Channel 用于 Goroutine 通信 ```go // 工作池模式 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() 的区别 ```go // 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{}` 就行。 ## 决策流程图 ```mermaid 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:切片共享底层数组 ```go 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 ```go 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 当精确值 ```go 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 |