goroutine泄漏和channel误用是微服务中两大典型并发问题:前者因协程未监听ctx.done()或未用waitgroup等待导致内存耗尽,后者因关闭混乱或无缓冲channel死锁引发panic或阻塞。

goroutine 泄漏:没给协程“出口”,它就永远卡着
微服务里启动 goroutine 很容易,但让它干净退出却常被忽略。泄漏的 goroutine 不会自动回收,几个小时后可能把内存吃光,而 runtime.NumGoroutine() 持续上涨是最直接信号。
典型错误是 HTTP handler 里裸起 goroutine 处理异步任务,却不绑定 context.Context 或用 sync.WaitGroup 等待:
go func() {
// 没检查 ctx.Done(),也没超时控制
result := heavyCalc()
saveToDB(result)
}()
- 所有长期运行的 goroutine 必须监听
ctx.Done(),用select做协作退出 - 若需等待完成,
sync.WaitGroup.Add(1)一定要在go语句前调用,否则极可能 panic 或静默失效 - 验证手段:访问
/debug/pprof/goroutine?debug=2,重点关注堆栈停留在chan receive或select的协程
channel 关闭混乱:谁关、何时关、关几次
向已关闭的 channel 发送数据会立刻 panic:send on closed channel;而多个 goroutine 都尝试 close(ch) 则可能 panic:close of closed channel。微服务中跨 goroutine 边界传递 channel 时尤其危险。
核心原则只有一条:发送方(且仅发送方)负责关闭,接收方绝不调用 close()。
- 用
v, ok := 判断 channel 是否已关闭并耗尽,<code>ok == false才表示真正结束 - 不要用
len(ch) == 0判断是否可发送——它只反映缓冲长度,和关闭状态无关 - 若 channel 是函数返回值(如流水线模式),应在 goroutine 内部
defer close(out),但必须确保该 goroutine 是唯一生产者
无缓冲 channel 死锁:发出去就卡住,没人接
微服务间模块解耦常用 channel 传递事件或命令,但无缓冲 channel 要求收发双方“同时就绪”。上游发完就走,下游因条件不满足跳过接收,就会导致发送方永久阻塞。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
例如库存扣减逻辑中,校验失败直接 continue,上游 goroutine 就再等不到接收:
for order := range orderCh {
if !checkInventory(order) {
log.Println("库存不足")
continue // 这里跳过,但 orderCh 还在等接收
}
deduct(order)
}
- 对关键路径上的发送操作,必须加超时保护:
select { case ch - 缓冲大小不能拍脑袋定:瞬时流量用 100–500,均衡负载参考 CPU 核心数 × 2,流水线阶段按处理耗时比配比
- 避免在 select 中只留一个
case且无default或ctx.Done(),否则极易卡死
闭包捕获循环变量:for 里起 goroutine,全用同一个 i
微服务批量处理请求或配置时,常写 for range 启一堆 goroutine,结果所有协程都读到循环末尾的变量值——这不是 bug,是 Go 变量作用域的确定行为。
错误写法:
for i := range tasks {
go func() {
process(tasks[i]) // i 总是 len(tasks)-1
}()
}
- 正确做法之一:在循环内显式复制变量
i := i,再进闭包 - 更推荐方式:把变量作为参数传入 goroutine 函数
go func(task Task) { process(task) }(tasks[i]) - 这个坑在线上压测时才暴露,调试成本高,务必在 Code Review 阶段盯住 for + go 组合
微服务的并发模型天然放大了 goroutine 生命周期和 channel 协作的复杂度。最易被忽略的是:channel 的关闭权责必须在设计阶段就约定清楚,而不是靠运行时 panic 来发现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










