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

11 KiB
Raw Blame History

tags, create time
tags create time
go
builtin
slice
map
channel
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