Go 的坑往往不在“语法太难”,而在“看起来简单但语义很细”。尤其当你从其它语言迁移过来,或者在并发、接口、切片、错误处理上沿用旧习惯时,最容易遇到一些隐蔽且代价高的 bug。

这篇文章按类别整理常见坑点,并给出相对稳妥的写法。你不需要一次背完,把它当作开发过程中可随时翻阅的 checklist 会更实用。

1. interface 的 nil:最经典的“看不见的坑”

1.1 “接口不为 nil,但里面的指针为 nil”

在 Go 里,interface 值由两部分组成:动态类型 + 动态值。只要动态类型不为空,interface 就不等于 nil,即使动态值是 nil 指针。

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string { return "oops" }

func mayReturnError() error {
    var e *MyError = nil
    return e
}

func main() {
    err := mayReturnError()
    fmt.Println(err == nil) // false
}

避坑建议:

常用修正:

func mayReturnError() error {
    return nil
}

或者把 *MyError 的构造与返回逻辑写得更显式。

2. range 变量捕获:循环里启动 goroutine 的陷阱

for range 中,迭代变量在每轮会被复用。闭包捕获的是“同一个变量”,不是“每次的值”。

for _, v := range []int{1, 2, 3} {
    go func() {
        fmt.Println(v) // 可能打印 3 3 3
    }()
}

稳妥写法:在循环体内创建新变量或把变量作为参数传入。

for _, v := range []int{1, 2, 3} {
    v := v
    go func() {
        fmt.Println(v)
    }()
}

// 或
for _, v := range []int{1, 2, 3} {
    go func(x int) {
        fmt.Println(x)
    }(v)
}

3. slice 的“引用语义”:append 导致的别名与意外修改

slice 是一个描述符(指针、长度、容量),底层数组可能被多个 slice 共享。

3.1 append 可能复用底层数组

当容量足够时,append 会在原数组上扩展,导致你以为“新 slice 不影响旧 slice”,但实际会互相影响。

避坑建议:

dst := append([]int(nil), src...)

3.2 预分配:性能优化也可能隐藏逻辑问题

常见写法:

items := make([]T, 0, n)
for ... {
    items = append(items, ...)
}

这很好,但要确保你理解 length 与 capacity 的差别,避免误用 make([]T, n) 造成“默认零值元素混入结果”。

4. map 的遍历顺序:永远不要依赖它

for range 遍历 map 的顺序是随机的(更准确说:不保证稳定)。

避坑建议:

keys := make([]string, 0, len(m))
for k := range m { keys = append(keys, k) }
sort.Strings(keys)
for _, k := range keys { fmt.Println(k, m[k]) }

5. string 与 rune/byte:中文处理最容易“看起来对但其实错”

Go 的 string 是字节序列(UTF-8),len(str) 是字节长度,不是字符数。

s := "上海"
fmt.Println(len(s))        // 6
fmt.Println(len([]rune(s))) // 2

避坑建议:

6. defer 的代价与循环:资源释放写对,但可能很慢

defer 在函数返回时执行,写在循环里不会“每轮立刻执行”。

for _, f := range files {
    fd, _ := os.Open(f)
    defer fd.Close() // 很多文件时会积累到函数结束
}

避坑建议:

for _, f := range files {
    func() {
        fd, _ := os.Open(f)
        defer fd.Close()
        // ...
    }()
}

7. 并发的“泄漏”:goroutine 不是免费的

7.1 channel 没人收/没人发导致永久阻塞

常见泄漏来源:

避坑建议:

7.2 data race:测试能过不代表没问题

数据竞争在压力上来之前可能不会暴露。

避坑建议:

8. 关闭 channel 的规则:谁创建谁关闭

错误的直觉:接收方觉得“读完就 close”。

一般规则:

9. time 与时区:Asia/Shanghai 只是第一步

你现在把 timezone 配置为 Asia/Shanghai 是对的,但工程里还要注意:

10. HTTP 客户端:默认用法容易踩坑

常见问题:

稳妥写法:

client := &http.Client{ Timeout: 5 * time.Second }
resp, err := client.Get(url)
if err != nil { return err }
defer resp.Body.Close()

11. 错误处理:不要只看 err,要看“语义”

Go 的错误处理看起来啰嗦,但它鼓励你把异常路径写清楚。

建议:

12. 一份实用 checklist

返回首页:欢迎访问

Go 语言的发展脉络