Go 中 new 和 make 的区别
new 与 make:内存分配双刃剑
在 Go 语言中,new 和 make 都用于分配内存,但它们的用途截然不同,适用于不同的数据类型。初学者常会混淆二者,本教程将彻底拆解它们的区别、内部机制和使用场景。
1. 核心差异速览
| 特性 | new(T) |
make(T, args...) |
|---|---|---|
| 适用类型 | 任意类型 | 仅 slice、map、channel |
| 返回值 | 指向 T 类型零值的指针 *T |
初始化后的 T 类型本身(非指针) |
| 初始化情况 | 只分配内存,置零,不初始化内部结构 | 分配并初始化内部数据结构,准备好使用 |
| 使用场景 | 值类型包装或需要零值指针时 | 需要立即使用的引用类型 |
简单记忆:想让一个引用类型(切片、映射、通道)真正能干活,就用 make;其他一切类型想要一个零值的指针,就用 new。
2. 深入理解 new:只分配,不构造
new 是 Go 的内置函数,它的原理非常简单:
- 为类型
T分配一块内存。 - 将该内存全部填充为
T类型的零值(如int为 0、string为 ""、指针为nil)。 - 返回该内存的地址,也就是一个
*T类型的指针。
代码示例:
// new 用于基本类型
p := new(int)
fmt.Println(*p) // 输出: 0
*p = 42
fmt.Println(*p) // 输出: 42
// new 用于自定义结构体
type Person struct {
Name string
Age int
}
pp := new(Person)
fmt.Printf("%+v\n", pp) // 输出: &{Name: Age:0}
// pp 是一个 *Person,可以直接修改字段
pp.Name = "Alice"
pp.Age = 30
注意:new 返回的指针指向的是零值对象。对于切片、映射和通道,情况就不一样了:
// ❌ 错误用法:new 出来的 slice、map 是 nil,无法直接使用
s := new([]int) // *[]int,指向一个 nil 切片
(*s)[0] = 1 // 运行时错误:panic: runtime error: index out of range [0] with length 0
m := new(map[string]int) // *map[string]int,指向一个 nil 映射
(*m)["key"] = 1 // 运行时错误:panic: assignment to entry in nil map
c := new(chan int) // *chan int,指向一个 nil 通道
// *c <- 1 // 死锁或panic,因为通道仍是 nil
new 为切片分配了头信息(指针、长度、容量)的内存,但头信息里的指针是 nil,底层数组不存在。映射和通道的内部结构指针也是 nil。因此这些零值对象无法直接使用。
3. 深入解析 make:分配、初始化,一步到位
make 也是内置函数,但只用于创建切片、映射和通道。它做三件事:
- 分配内存。
- 初始化内部数据结构(例如切片的底层数组、映射的哈希桶、通道的缓冲区)。
- 返回初始化后的值(非指针),这个值可以立即安全使用。
3.1. 创建切片
// make([]T, length, capacity)
s := make([]int, 3, 5)
fmt.Println(s) // 输出: [0 0 0]
fmt.Println(len(s)) // 输出: 3
fmt.Println(cap(s)) // 输出: 5
// 可以直接赋值
s[0] = 10
s = append(s, 100) // 追加元素
make 会为切片分配一个长度为 5 的整型数组作为底层存储,并返回一个长度为 3 的切片指向它。
3.2. 创建映射
// make(map[KeyType]ValueType, [capacity])
m := make(map[string]int, 10) // 可以指定初始容量
m["age"] = 30
fmt.Println(m["age"]) // 输出: 30
// 映射已就绪,不会 panic
3.3. 创建通道
// 无缓冲通道
ch1 := make(chan int)
// 带缓冲通道
ch2 := make(chan string, 5)
go func() {
ch1 <- 1
}()
fmt.Println(<-ch1) // 输出: 1
通道必须用 make 初始化,否则只是声明为 var ch chan int 会得到 nil 通道,往 nil 通道发送或接收会永久阻塞(或导致死锁)。
4. 关键区别与常见误解
4.1. 返回值类型不同
p := new(int) // *int
v := make([]int, 0) // []int (不是指针)
理解了为什么 make 返回的不是指针吗?因为切片、映射、通道本身就是引用类型,它们的“值”已经包含了指向底层数据的指针。你直接传递这个值即可共享数据,无需再取地址(尽管你仍然可以取 &slice,但那是指向切片头结构的指针,与底层数据无关)。
4.2. 能否用 new + 解引用代替 make?
对于切片,你可以用 new 再解引用,得到一个零值切片,但那是 nil,毫无意义:
sNil := *new([]int) // 完全等价于 var sNil []int
这种写法只会让代码更混乱,官方不建议。
5. 实战:什么时候用哪个?
使用 new:
- 你需要一个指向某个值类型的指针,并希望它为零值。例如,在 JSON 反序列化中区分“未设置”和“零值”时,可能会用
*int、*bool等字段。 - 暂时不需要初始化,或者零值就有用(如
sync.Mutex,零值是未加锁状态,可以直接用,无需专门初始化)。
var mu *sync.Mutex
mu = new(sync.Mutex)
mu.Lock()
使用 make:
- 你需要一个能立刻添加数据或进行通信的
map。 - 你准备使用切片并需要指定长度或容量。
- 你需要使用
channel进行 goroutine 间通信。
只要用到这三个引用类型并且想直接使用它们,就必须用 make。
6. 总结
new 和 make 的分工明确了 Go 语言的内存管理哲学:普通值类型和用户定义类型通过 new 获得零值指针;而切片、映射和通道这三种内置数据结构,因其复杂性,需要专属的 make 来负责初始化。牢牢记住这张对比表,你将再也不会在二者间误用。
| 函数 | 输入 | 输出 | 适用类型 |
|---|---|---|---|
new(T) |
类型 | *T 零值指针 |
任何类型 |
make(T, ...) |
类型、长度/容量 | 已初始化的 T |
slice / map / chan |
现在,打开你的编辑器写几行代码验证一下吧。亲手试验是内化这个概念的唯一捷径。