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

332 lines
11 KiB
Markdown
Raw Normal View History

2026-05-14 16:11:17 +08:00
---
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 |