Go 常见的坑,一次说清
这些坑大多来自 Go 的”值语义”和”并发模型”,踩过一次就懂了。以下每条都附修复方式。
1. for range 的变量副本
循环变量是每次迭代复用的同一个变量(Go 1.21 及更早),循环内取地址会得到同一个指针:
var out []*int
for _, v := range []int{1, 2, 3} {
out = append(out, &v) // 错:所有元素都指向同一个 v
}
修复:显式建局部变量或改用索引。
for i := range list {
v := list[i] // 新变量
out = append(out, &v)
}
📌 Go 1.22 起循环变量每次迭代都是新变量,这个坑在新版本里已修复——但升级前的老代码仍要留意,且 goroutine 闭包捕获(go func() { fmt.Println(v) }())的同类问题也要检查。
2. Channel 的关闭与阻塞
- 向已关闭的 channel 发送会 panic,且 panic 发生在发送方 goroutine,recover 不一定来得及;
- 无缓冲 channel 的永久阻塞:接收方没就绪时发送方阻塞,超时场景下子 goroutine 会泄露:
ch := make(chan int)
go func() { ch <- 1 }()
select {
case <-ch: // 正常
case <-time.After(1 * time.Second): // 超时了,但子 goroutine 还在阻塞
}
修复:用带缓冲 channel、用 context 控制生命周期、或”发送方 select + done”模式。关闭 channel 的责任约定为只有发送方可以关闭。
3. Slice 的”隐藏”底层数组
切片操作共享底层数组,小切片可能”钉住”大数组,导致内存无法回收:
func leak() []byte {
bigData := make([]byte, 1e6)
return bigData[:10] // 底层 1MB 数组仍被引用,GC 无法释放
}
修复:返回前 copy 出需要的数据;或先 append([]byte(nil), bigData[:10]...)。
4. 错误处理与 panic 滥用
Go 鼓励显式错误处理,两个常见反面:忽略 error 返回值(用 _ 吞掉,出问题时无从排查)和滥用 panic(配置加载失败这类可预期错误应返回 error,而不是 panic 崩掉整个进程)。原则:panic 只用于程序不可恢复的状态(如初始化断言失败)。
5. Goroutine 泄露
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func() {
defer wg.Done() // 漏写这行,Wait() 永远阻塞
process(task)
}()
}
wg.Wait()
排查手段:go tool pprof goroutine 或 runtime.NumGoroutine 监控;代码审查重点看 wg.Add/Done 配对、channel 关闭、context 取消链。
6. nil 接口不等于 nil 指针
var buf *bytes.Buffer // 动态值为 nil
var w io.Writer = buf // 接口非 nil:动态类型是 *bytes.Buffer!
if w == nil { // false,即使 buf 是 nil
// 这段不会执行
}
经典坑:函数返回 nil *T 但签名是 error/接口类型,调用方判断”err == nil”永远为假。修复:返回前显式判断,nil 时返回 nil 接口。
7. 循环里的 defer
for _, p := range persons {
mutex.Lock()
defer mutex.Unlock() // 错:所有锁到函数末尾才释放
p.Age = 13
}
修复:把循环体包进匿名函数,让 defer 每次迭代都执行。
for _, p := range persons {
func() {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}()
}
如何避免掉坑
- 代码审查:重点盯并发操作、资源管理(锁/连接/文件)、错误处理;
- 工具辅助:
go vet静态检查 +-race竞争检测,CI 里常开; - 边界测试:空切片、nil 接口、超时、关闭的 channel 都写用例。


