原文地址 mp.weixin.qq.com
读懂 slice / map / channel 为什么必须 make —— 官方定义、Go 规范、Effective Go 与高频坑,一次讲透
Go 里有三种类型比较特殊:slice、map、channel。它们的变量本质是"指向底层数据结构的描述符"——slice 是「指针 + 长度 + 容量」的三元组,map 和 channel 背后也都是运行时维护的结构。这些结构如果不先初始化,变量就是 nil,一用就崩。
make 干的活就是:把这三类内部结构搭好,返回一个开箱即用的值。理解 make 只需要抓住一句话——它只服务这三种类型,返回的是值本身,不是指针。
一、builtin 包:make 的官方定义#
这是最权威、最精炼的一份,写在 builtin 包的源码注释里:
func make(t Type, size ...IntegerType) Type
The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make’s return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:
make 这个内置函数只干一件事:分配并初始化 slice、map、channel 这三种类型的对象,就这三种。第一个参数和 new 一样传“类型”而不是“值”,但跟 new 不同,make 返回的也是这个类型本身,而不是指向它的指针。具体返回啥,取决于类型:
Slice: The size specifies the length. The capacity of the slice is equal to its length. A second integer argument may be provided to specify a different capacity; it must be no smaller than the length. For example,
make([]int, 0, 10)allocates an underlying array of size 10 and returns a slice of length 0 and capacity 10 that is backed by this underlying array.
Slice:size 指定的是长度,容量默认等于长度。可以再传第二个整数单独指定容量,但容量不能比长度小。比如 make([]int, 0, 10) 会分配一个 10 个元素的底层数组,返回长度 0、容量 10、背靠这个数组的 slice。
Map: An empty map is allocated with enough space to hold the specified number of elements. The size may be omitted, in which case a small starting size is allocated.
Map:分配一个空 map,预留能装下指定数量元素的空间。size 可以省略,省略就只给一个很小的起始空间。
Channel: The channel’s buffer is initialized with the specified buffer capacity. If zero, or the size is omitted, the channel is unbuffered.
Channel:缓冲区按你指定的容量初始化。容量是 0,或者省略 size,得到的就是无缓冲 channel。
allocates and initializes(分配并初始化):不只是划一块内存(那是 new 的活),还要把内部结构搭好,让值立刻能用。
underlying array(底层数组):slice 背后真正存数据的那块连续内存,slice 只是它的“视图”。
unbuffered channel(无缓冲 channel):容量为 0 的 channel,发送和接收必须同时就绪才能完成。
二、Go 规范:Making slices, maps and channels#
Go 规范原文:https://go.dev/ref/spec#Making_slices_maps_and_channels
规范(spec)给的定义更正式、更完整,还专门覆盖了泛型场景。先看开头:
The built-in function make takes a type T, which must be a slice, map or channel type, or a type parameter, optionally followed by a type-specific list of expressions. It returns a value of type T (not *T). The memory is initialized as described in the section on initial values.
make 接收一个类型 T——必须是 slice、map、channel,或者一个类型参数(type parameter)——后面可以跟一组该类型专属的表达式。它返回一个 T 类型的值(不是 *T),内存按“初始值”那一节的规定初始化。
规范用一张表把每种调用形式的结果列得很清楚:
| 调用方式 | 类型T | 结果 |
|---|---|---|
| make(T,n) | slice | slice of type T with length n and capacity n |
| make(T, n, m) | slice | slice of type T with length n and capacity m |
| make(T) | map | map of type T |
| make(T, n) | map | map of type T with initial space for approximately n elements |
| make(T) | channel | unbuffered channel of type T |
| make(T, n) | channel | buffered channel of type T, buffer size n |
| make(T, n) | type parameter | see below |
| make(T, n, m) | type parameter | see below |
紧接着规范解释了泛型和 size 参数的约束:
If the first argument is a type parameter, all types in its type set must have the same underlying type, which must be a slice or map type, or, if there are channel types, there must only be channel types, they must all have the same element type, and the channel directions must not conflict.
如果第一个参数是类型参数(泛型场景),那它类型集里的所有类型必须有相同的底层类型——要么都是 slice 或 map;如果是 channel,就只能全是 channel,元素类型还得一样,方向也不能冲突。
Each of the size arguments n and m must be of integer type, have a type set containing only integer types, or be an untyped constant. A constant size argument must be non-negative and representable by a value of type int; if it is an untyped constant it is given type int. If both n and m are provided and are constant, then n must be no larger than m. For slices and channels, if n is negative or larger than m at run time, a run-time panic occurs.
size 参数 n 和 m 必须是整数类型,或者是只含整数类型的类型集,或者是一个无类型常量。常量形式的 size 必须非负、能用 int 表示;如果是无类型常量,就按 int 处理。n 和 m 若都是常量,n 不能大于 m。对 slice 和 channel 来说,运行时 n 为负数或大于 m,会 panic。
规范还给了几个正例和反例(这几行代码是原文示例,含移位运算符,原样贴出):
s := make([]int, 10, 100) // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3) // slice with len(s) == cap(s) == 1000
s := make([]int, 1<<63) // illegal: len(s) is not representable by a value of type int
s := make([]int, 10, 0) // illegal: len(s) > cap(s)
c := make(chan int, 10) // channel with a buffer size of 10
m := make(map[string]int, 100) // map with initial space for approximately 100 elements`Calling make with a map type and size hint n will create a map with initial space to hold n map elements. The precise behavior is implementation-dependent.
用 map 类型调用 make 并给个 size 提示 n,会创建一个能装下 n 个元素的初始 map。具体行为由实现决定(不同版本、不同平台可能略有差异)。
type parameter(类型参数):泛型写法里的那个 T,比如 func f[T any] 里的 T。
type set(类型集):类型参数约束里允许的所有类型的集合。
untyped constant(无类型常量):还没确定具体类型的常量,比如字面量 100。
buffered channel(有缓冲 channel):容量大于 0 的 channel,缓冲区没满时发送不阻塞。
三、Effective Go:make 和 new 的分工#
Effective Go 原文:https://go.dev/doc/effective_go#allocation_make
Effective Go 用一节专门讲 make 和 new 为什么不是一回事:
Back to allocation. The built-in function make(T, args) serves a purpose different from new(T). It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T). The reason for the distinction is that these three types represent, under the covers, references to data structures that must be initialized before use. A slice
for example, is a three-item descriptor containing a pointer to the data (inside an array), the length, and the capacity, and until those items are initialized, the slice is nil. For slices, maps, and channels, make initializes the internal data structure and prepares the value for use.
内置函数 make(T, args) 和 new(T) 干的不是一回事。它只创建 slice、map、channel 这三种类型,返回的是“已初始化”(initialized)的值,类型是 T 而不是 *T,也不是“零值”(zeroed)。为什么要分这么开?因为这三种类型在底层其实是“对某个数据结构的引用”,这个结构用之前必须先初始化。拿 slice 举例:它是一个三元描述符,装着「指向数据(在数组里)的指针、长度、容量」三项,这三项没初始化之前,slice 就是 nil。对这三种类型,make 负责把内部结构初始化好,让值处于可用状态。
它给了一段对比 new 和 make 的代码:
// allocates slice structure; *p == nil; rarely useful
var p *[]int = new([]int)
// the slice v now refers to a new array of 100 ints
var v []int = make([]int, 100)
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)然后是一句必须记住的结论:
Remember that make applies only to maps, slices and channels and does not return a pointer. To obtain an explicit pointer allocate with new or take the address of a variable explicitly.
记住:make 只适用于 map、slice 和 channel,而且不返回指针。想显式要一个指针,就用 new 分配,或者直接对变量取地址。
initialized vs zeroed(已初始化 vs 零值):initialized 指内部结构搭好了、能用;zeroed 指只把内存清零,内部结构还是 nil。
reference types(引用类型):底层是“对数据结构的引用”的类型,Go 里就是 slice、map、channel 这仨,make 只管它们。
three-item descriptor(三元描述符):slice 的底层表示——指针、长度、容量。
四、make vs new 一张表说清#
| 对比 | make(T ,args…) | new(T) |
|---|---|---|
| 适用类型 | 仅slice / map / channel | 任意类型 |
| 返回 | T(值本身) | 任意类型 |
| 状态 | 已初始化 | 零值 |
| 典型用途 | 初始化三种引用类型 | 分配一块零值内存拿指针 |
这俩不是能互相替代的关系:new 能对任意类型下手,但 new 出来的 slice、map、channel 是 nil,还是得靠 make 初始化;反过来,make 压根不对这三种以外的类型开放。
五、三种类型各自的语义和示例#
5.1 slice:长度和容量#
package main
import "fmt"
func main() {
a := make([]int, 5) // len=5, cap=5(容量省略时 = 长度)
b := make([]int, 5, 10) // len=5, cap=10
c := make([]int, 0, 10) // len=0, cap=10,先声明后 append 的常见姿势
fmt.Println("a:", len(a), cap(a))
fmt.Println("b:", len(b), cap(b))
fmt.Println("c:", len(c), cap(c))
}运行结果:
a: 5 5
b: 5 10
c: 0 10len 是“现在有几个元素”,cap 是“底层数组总共多大”。第三个例子 len=0、cap=10 是工程里最常见的姿势:先把容量占好,后面 append 就不会频繁触发扩容。
5.2 map:容量提示不是上限#
package main
import "fmt"
func main() {
m := make(map[string]int)
m["alice"] = 30
m["bob"] = 25
fmt.Println(m, "len =", len(m))
// 第二个参数是"容量提示",不是上限,map 会自动扩容
n := make(map[string]int, 100)
n["x"] = 1
fmt.Println("n len =", len(n))
}运行结果:
len = 2
n len = 1map 的第二个参数只是“初始空间提示”,装满了照样自动扩容,不会像 slice 那样被 cap 卡住。
5.3 channel:缓冲 vs 无缓冲#
先看有缓冲的。这段代码用到了 channel 的收发运算符 <-,原样逐行贴出:
package main
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}运行结果:
12容量 2 的缓冲 channel,塞两个不阻塞,取两个也顺利。
再看无缓冲的。发送和接收必须同时就绪,这里用一个 goroutine 发 42,主 goroutine 接收,正好配对:
package main
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch)
}运行结果:
42make(chan int) 省略容量,等价于 make(chan int, 0),得到的就是无缓冲 channel。
六、三个高频坑#
6.1 nil slice 能 append,nil map 不能写#
package main
import "fmt"
func main() {
var s []int // nil slice
s = append(s, 1, 2, 3)
fmt.Println(s, "len =", len(s))
}运行结果:
[1 2 3] len = 3package main
func main() {
var m map[string]int // nil map
m["a"] = 1 // 运行到这里 panic
}运行结果:
panic: assignment to entry in nil map为什么 slice 的 nil 能用、map 的 nil 不能?因为
append 遇到 nil slice 会当成长度为 0 的 slice 处理,自动分配底层数组;而 map 的写操作没有这种兜底,直接 panic。所以 map 必须 make 或用字面量初始化。
6.2 nil channel 会永久阻塞#
var ch chan int 声明出来的 channel 是 nil,对它 ch <- 1 收发都会永久阻塞,直接死锁。这也是为什么 channel 一定要 make。
6.3 容量必须不小于长度#
make([]int, 5, 3) 这种写法,容量比长度小,编译直接报错(invalid argument)。规范示例里的 make([]int, 10, 0) 也标注了 illegal。
七、泛型里的 make(Go 1.18+)#
规范里提到 make 的第一个参数可以是 type parameter。比如写一个泛型函数,用 make 造一个 T 类型的 slice:
`package main``import "fmt"``func dup[T any](n int) []T {``return make([]T, n)``}``func main() {` `fmt.Println(dup[int](3))``}`运行结果:
[0 0 0]约束是:类型集里的类型要么都是 slice/map,要么全是同元素类型、同方向的 channel,否则编译不过。
