Files
leetcode-go/note/2026-05-14/01-make.md
T

332 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 |