实战应用问题速查
本页汇总 Go 上线后最常踩的应用级问题,每条按"现象 → 原因 → 修复"组织,可作为排查手册收藏。涉及更系统内容的地方会链接到对应章节。
1. goroutine 泄漏:服务内存缓慢上涨
- 现象:运行数天后内存持续增长,重启恢复;
pprof goroutine显示数万个 goroutine 阻塞在同一行。 - 原因:goroutine 阻塞在无接收方的通道发送或永不关闭的通道接收上,永远无法退出。
- 修复:为阻塞操作提供超时或取消出口。
定位手段见可观测性:curl /debug/pprof/goroutine?debug=1 直接看每个 goroutine 的栈。
2. 切片共享底层数组:改"副本"却改了原数据
- 现象:从切片切出的"子切片"被修改后,原切片的数据也变了;或
append后数据互相覆盖。 - 原因:切片是指向底层数组的视图,
s[low:high]与append都可能共享内存(详见切片)。 - 修复:需要独立副本时显式克隆。
示例需 import "slices"(Go 1.21+)。
3. typed nil:接口不等于 nil
- 现象:函数返回了"nil 指针",调用方
if err != nil判断却走进了非 nil 分支。 - 原因:接口值由 (类型, 值) 二元组构成。把 nil 的具体指针赋给接口后,接口的类型信息非空,因此接口本身不等于 nil。
- 修复:直接返回
nil(显式),或返回值保持接口类型声明:
4. map 并发读写:偶发 panic
- 现象:压测或高并发下随机 panic:
fatal error: concurrent map writes。这类 panic 无法 recover。 - 原因:map 非并发安全(详见sync 包),并发读写直接触发运行时致命错误。
- 修复:
sync.Mutex包裹(通用)、sync.RWMutex(读多写少)、sync.Map(键集稳定)或分片锁。用go test -race在上线前暴露。
5. 循环中的 defer:资源堆积
- 现象:批量处理文件时句柄耗尽(
too many open files)。 - 原因:
defer到函数返回才执行,循环内的 defer 会一直堆积。
6. 循环里的 time.After:定时器堆积
- 现象:高频循环中内存与定时器数量持续增长。
- 原因:每次循环
time.After都新建一个不会提前回收的定时器,而循环周期远短于超时值。
低频路径(每个请求一次)用 time.After 完全没问题;高频循环必须复用 Timer。
7. 错误被覆盖或吞掉
- 现象:失败时日志里的错误"不是根因",或错误直接消失。
- 原因:连续两次赋值
err,第二次覆盖第一次;或用_忽略。
同时遵守两条纪律:不用 _ 丢弃 error(交给 errcheck 盯);包装错误用 %w 而非 %v(errorlint 强制检查)。
8. HTTP 客户端三连坑
- 现象:连接数耗尽、请求永不返回、并发下句柄暴涨。
- 原因与修复(详见 REST 客户端):
9. context 断链:取消信号传不下去
- 现象:客户端断开后,下游数据库查询仍在执行;上游超时了,整条链路却还在烧资源。
- 原因:某层函数不接收
ctx,或调用下游时换了新的context.Background()。 - 修复:约定所有 I/O 函数第一个参数是
ctx,调用下游必须用收到的同一个 ctx 派生(见context)。
10. JSON 数字精度丢失
- 现象:前端传来的 64 位订单 ID 解码后末位变成 0。
- 原因:
map[string]any或any字段中的 JSON 数字默认解码为float64,有效整数仅到 2⁵³(详见 JSON 处理)。 - 修复:大整数用结构体字段(
int64)接收;确需动态解析时用json.Decoder.UseNumber()。
速查总表
Tip
这份清单的另一半防线是机器:go vet -race、golangci-lint(errorlint/noctx/bodyclose)、pprof 巡检会拦下其中大半。工程规约见工程化。