Go 的“陷阱”

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 都写用例。
滚动至顶部