刚接触 Go 时,new 和 make很容易被当成“两个都能申请内存的函数”来记忆,但一到写结构体、切片或映射时,差别马上就会暴露出来。本文不绕概念,直接从返回值、适用类型和常见报错入手,把它们各自该用在什么地方讲清楚,帮助你在代码里快速判断该选哪一个。
new 和 make 的核心区别
在 Go 语言中,new 和 make都和内存分配有关,但它们解决的不是同一类问题。
new做的是“为某个类型分配一块内存,并返回这个类型零值的指针”。它接收的是类型,而不是值,函数签名如下:
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
也就是说,new返回的一定是指针,指向的是该类型的零值。比如 int 的零值是 0,string 的零值是 "",而指针、切片、映射、通道、函数这些引用相关类型的零值则是 nil。
相比之下,make不是单纯分配内存,而是“为特定类型完成初始化”。它只用于 slice、map 和 channel 三种类型,返回的是类型本身,而不是指针。这个差别正是日常开发里最需要优先记住的一点。
new 是什么:返回指针与零值
先看一个最简单的例子:
package main
import (
"fmt"
)
func main() {
s := new(string)
fmt.Println(s) // output: 0xc00012a0b0
fmt.Println(*s) // output:
}
new(string)返回的是 *string,也就是字符串指针。打印 s 时看到的是地址,而 *s 对应的值则是字符串零值,也就是空字符串。
这个行为在结构体上也一样:
package main
import (
"fmt"
)
type User struct {
Name string
Age int
}
func main() {
var u *User
u = new(User)
u.Name = "alice"
u.Age = 18
fmt.Println(u) // output: &{alice 18}
fmt.Println(*u) // output: {alice 18}
fmt.Println(&u) // 0xc00005a040
}
这里的 u 是 *User。如果只声明不初始化,它默认是 nil,不能直接拿来写字段;经过 new(User) 之后,才有了一块可用内存,接下来才能安全赋值。
不过在实际项目里,new并不是创建结构体指针的唯一写法,更常见的是直接取结构体字面量地址:
package main
type User struct {
Name string
Age int
}
func main() {
var u *User
u = &User{}
}
new(User)和&User{}在日常使用里几乎没有本质区别,后者通常更常见,代码也更紧凑,还方便在初始化时直接填写字段值。因此在业务代码中,很多开发者会优先写成 &User{}。
make 为什么只能用在三种类型
make只服务于三类内置引用结构:slice、map 和 channel。原因在于,这三种类型不是只要“有一块内存”就能直接工作,它们还需要完成内部结构的初始化。
看下面的例子:
package main
import "fmt"
func main() {
s := make([]int, 4, 16) // size: 4, cap: 16
fmt.Println(s) // output [0 0 0 0]
m := make(map[string]int, 4) // size: 4
m["k0"] = 1
fmt.Println(m) // output: map[key0:1]
ch := make(chan int, 1) // size: 1
ch <- 10
fmt.Println(<-ch) // output: 10
}
这段代码体现了 make 的三个关键特征:
- 它只适用于
slice、map、channel。 - 它返回的是可直接使用的值,而不是指针。
- 它可以顺带完成初始化参数设置,比如切片长度、切片容量、映射初始空间、通道缓冲大小。
这也是为什么在 Go 项目里,make的出现频率通常远高于 new。切片、映射和通道本来就是高频数据结构,而它们又往往必须靠 make 才能进入可用状态。
用 new 处理 slice、map、channel 会发生什么
很多初学者会继续追问:既然 new 也能分配内存,那它能不能直接拿来创建 slice、map 或 channel?答案是可以写,但通常不能直接用。

先看 map 的例子:
package main
import "fmt"
func main() {
m := new(map[string]string) // m 的类型为 *map[string]string
fmt.Println(&m) // 0xc00005a040
fmt.Println(*m == nil) // true
(*m)["k0"] = "v0" // panic: assignment to entry in nil map
}
这里 new 返回的是 *map[string]string,也就是“指向 map 的指针”。但要注意,指针指向的那个 map 值仍然是 nil,并没有变成一个已经初始化好的映射。因此一旦写入键值对,就会触发:
panic: assignment to entry in nil map
再看三个类型统一对比的结果:
package main
import "fmt"
func main() {
a := *new([]int)
fmt.Printf("%v, %tn", a, a == nil) // output: [], true
b := *new(map[string]int)
fmt.Printf("%v, %tn", b, b == nil) // output: map[], true
c := *new(chan int)
fmt.Printf("%v, %tn", c, c == nil) // output: <nil>, true
}
无论是 slice、map 还是 channel,new 得到的都只是“这个类型零值的指针”。而这三类类型的零值恰好都是 nil,因此不能等价替代 make 的初始化效果。
这正是两者最容易踩坑的地方:new 解决的是“有无一块内存”,make 解决的是“这个数据结构能不能直接工作”。
开发中怎么选择:new、make 与直接字面量
如果把实际开发场景压缩成一句判断规则,可以这样记:
- 创建基本类型或结构体指针,优先考虑
new或&T{}。 - 创建
slice、map、channel,优先使用make。 - 如果是切片、映射的简单空值初始化,也可以直接使用字面量。
例如:
package main
import "fmt"
func main() {
s := []int{}
fmt.Println(s)
m := map[string]string{}
fmt.Println(m)
}
这种写法可以替代一部分 make 场景,尤其是在不关心预分配长度、容量时,代码会更直观。
但有一个例外需要单独记住:chan 必须通过 make 初始化。仅仅使用 var 声明通道时,它的默认零值是 nil,不能正常收发数据。
至于 new(User) 和 &User{} 的差别,通常不需要在普通业务代码里过度放大。前者表达“分配一个零值对象并返回指针”,后者表达“创建一个结构体并取地址”,最终效果在大多数场景中都足够接近。真正更重要的,是你是否清楚某个类型拿到零值后能不能直接使用。
归纳起来:
new:分配内存,返回指针,得到的是零值对象。make:初始化slice、map、channel,返回可直接使用的值。
把这条线分清,写 Go 时就不容易在指针、空 map 和 nil channel 上反复踩坑。










