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
}
避坑建议:
- 如果函数签名返回
error,确保“没有错误”时返回真正的nil。 - 用
var err error = nil并不能解决,关键在于不要把“带类型的 nil 指针”塞进 interface。
常用修正:
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”,但实际会互相影响。
避坑建议:
- 需要“真正拷贝”时,用
copy或append([]T(nil), s...)创建新底层数组。
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 的顺序是随机的(更准确说:不保证稳定)。
避坑建议:
- 要稳定输出:把 key 拿出来排序,再按排序后的 key 访问 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
避坑建议:
- 需要按“字符”处理:用
[]rune或utf8.RuneCountInString - 需要按“字节协议”处理:用
[]byte
6. defer 的代价与循环:资源释放写对,但可能很慢
defer 在函数返回时执行,写在循环里不会“每轮立刻执行”。
for _, f := range files {
fd, _ := os.Open(f)
defer fd.Close() // 很多文件时会积累到函数结束
}
避坑建议:
- 循环中管理资源:显式 Close,或者把每轮逻辑提到一个小函数里让 defer 作用域变小。
for _, f := range files {
func() {
fd, _ := os.Open(f)
defer fd.Close()
// ...
}()
}
7. 并发的“泄漏”:goroutine 不是免费的
7.1 channel 没人收/没人发导致永久阻塞
常见泄漏来源:
- goroutine 等待发送,但接收方提前退出
- goroutine 等待接收,但发送方永远不会再发
避坑建议:
- 用
context.Context做取消传播 - 确保所有路径都能退出(包括错误分支、超时分支)
- 对外部 I/O 一定要设置超时
7.2 data race:测试能过不代表没问题
数据竞争在压力上来之前可能不会暴露。
避坑建议:
- 开发/CI 里长期跑
-race - 共享数据要么只在一个 goroutine 里访问,要么用锁/原子/通道串行化
8. 关闭 channel 的规则:谁创建谁关闭
错误的直觉:接收方觉得“读完就 close”。
一般规则:
- 发送方知道“什么时候不再发送”,所以由发送方关闭
- 多个发送方场景不要随便 close,通常用额外的协调机制(WaitGroup、context、单独的 close goroutine)
9. time 与时区:Asia/Shanghai 只是第一步
你现在把 timezone 配置为 Asia/Shanghai 是对的,但工程里还要注意:
- 不要把时间当字符串传递,尽量传 Unix 时间戳或 RFC3339
- 数据库存储尽量用 UTC,展示时再转本地时区
- 解析时间要显式指定 location(尤其是 legacy 数据)
10. HTTP 客户端:默认用法容易踩坑
常见问题:
- 没有超时:请求卡住拖垮 goroutine
- 忘记关闭 response body:连接无法复用,资源泄漏
稳妥写法:
client := &http.Client{ Timeout: 5 * time.Second }
resp, err := client.Get(url)
if err != nil { return err }
defer resp.Body.Close()
11. 错误处理:不要只看 err,要看“语义”
Go 的错误处理看起来啰嗦,但它鼓励你把异常路径写清楚。
建议:
- 错误要带上下文:
fmt.Errorf("xxx: %w", err) - 能分类就分类:用 sentinel error 或自定义错误类型
- 对外返回时注意不要泄漏内部细节(尤其是安全相关)
12. 一份实用 checklist
error返回值要保证“无错即 nil”- range + goroutine 必须传参或重新绑定变量
- slice 需要拷贝时一定显式 copy
- map 遍历永远不依赖顺序
- 字符串处理区分 byte 与 rune
- 循环中 defer 需注意作用域与资源释放时机
- 并发必配超时/取消(context)
- channel 遵循“发送方关闭”
- HTTP 必设超时,必关闭 Body
返回首页:欢迎访问