make 仅用于切片、map、chan,返回已初始化的值;new 适用于任意类型,返回指向零值的指针。

make 和 new 的区别不是“要不要初始化”,而是「类型适用范围」和「返回值语义」完全不同。
make 只能用于 []T、map[K]V、chan T 这三种引用类型,返回的是已初始化的值本身(比如一个可直接使用的切片),底层会分配底层数组、哈希表或环形缓冲区,并设置好长度、容量等字段。
new 接受任意类型,返回的是指向该类型的零值指针(<em>T</em>),不区分是否是引用类型;它只做内存分配 + 置零,不构造逻辑结构。比如 new([]int) 返回的是 []int,但这个切片本身仍是 nil,不能直接 append。
常见错误:
立即学习“go语言免费学习笔记(深入)”;
- 用
new(map[string]int)以为能得到一个空 map —— 实际得到的是*map[string]int,解引用后仍是nil,写入 panic - 用
make([]int, 0, 10)后误以为容量是“预留空间”—— 容量只是当前底层数组可用长度,append超过时仍会扩容,不是固定上限
for range 中的迭代变量重用问题常被低估
Go 在 for range 循环中复用同一个变量地址,每次迭代只更新其值。这在闭包或 goroutine 中直接捕获该变量时会导致所有协程看到最后一个值。
例如:
for i := range []int{0, 1, 2} {
go func() { fmt.Println(i) }()
}
输出很可能是 2 2 2,而非 0 1 2。
解决方式只有两种:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 显式传参:
go func(i int) { fmt.Println(i) }(i) - 在循环内声明新变量:
val := i; go func() { fmt.Println(val) }()
注意:range 遍历 map 时,顺序不保证,且每次迭代的 key/value 是拷贝值,不是引用;但遍历 slice 时,v 是元素拷贝,修改 v 不影响原 slice,要改原数据必须通过索引 s[i] = ...。
defer 修改返回值只在有名返回值下生效
函数签名带命名返回参数(如 func f() (err error))时,return 语句隐式赋值给 err,随后 defer 函数可读写该变量,从而改变最终返回值。
但若函数是无名返回(如 func f() error),return err 会把 err 拷贝进一个匿名临时变量,defer 修改的是自己捕获的参数副本,不影响那个临时变量。
更隐蔽的坑是:即使是有名返回,如果 defer 中以值方式接收该变量(如 defer func(e error) { e = fmt.Errorf("wrap") }(err)),依然改不动——因为传参是值拷贝。必须直接访问函数作用域里的 err 变量。
nil map 和 empty map 行为差异直接影响 panic 风险
var m map[string]int 是 nil map;m = make(map[string]int) 是 empty map。两者都长度为 0,但:
- 对 nil map 读操作(
v, ok := m["k"])安全,返回零值和 false - 对 nil map 写操作(
m["k"] = 1)直接 panic:assignment to entry in nil map - delete(
nil map, key) 安全,无效果
所以初始化检查不能只看 len(m) == 0,而应判断 m == nil。sync.Map 等并发安全封装内部也依赖此区分,不会对 nil 做写操作。
实际编码中最容易忽略的,是把 map 作为结构体字段却忘记在 struct 初始化时用 make 构造它——字段默认是 nil,首次写就崩。

















