goroutine泄漏典型代码是向无缓冲channel发送数据但无接收者,导致goroutine永久阻塞;常见于后台监控未加context取消、循环启goroutine无数量控制、http服务未优雅关闭等场景。

Go 并发面试题不是考你会不会写 go func(),而是看你能不能避开 goroutine 泄漏、channel 死锁、数据竞争这三类真实线上问题。
goroutine 泄漏的典型代码长什么样
泄漏往往藏在“看起来很合理”的启动逻辑里。比如启动一个后台监控 goroutine,但没给退出信号,主程序结束后它还在跑;或者用 for 循环不断启 goroutine,却没控制总数或生命周期。
- 常见错误模式:
go http.ListenAndServe(":8080", nil)后直接 return,没做 graceful shutdown,服务重启时旧 goroutine 残留 - 循环启动不加限制:
for _, url := range urls { go fetch(url) }—— 如果urls是百万级,就等于瞬间创建百万 goroutine,调度器压垮,内存暴涨 - channel 接收端缺失:向一个没人接收的无缓冲 channel 发送数据,发送 goroutine 永久阻塞(即泄漏)
- 修复思路:用
context.Context传递取消信号;用sync.WaitGroup等待明确数量的 goroutine;对 channel 操作前确认接收方存在或使用select+default防阻塞
无缓冲 channel 阻塞的准确触发条件
无缓冲 channel 的阻塞不是“发不出去”,而是“没人等着接”。它要求发送和接收操作在**同一时刻就绪**,否则任一方都会挂起。
- 发送阻塞:当
ch 执行时,<code>ch是无缓冲的,且当前没有 goroutine 在执行—— 发送方会一直等,直到有接收者出现 - 接收阻塞:当
执行时,<code>ch是无缓冲的,且当前没有 goroutine 在执行ch —— 接收方会一直等,直到有发送者出现 - 注意:nil channel 永远阻塞(
var ch chan int后直接),而关闭后的无缓冲 channel 接收会立即返回零值,发送则 panic - 调试技巧:用
runtime.NumGoroutine()对比前后数量,暴增说明可能有 goroutine 卡在 channel 上
select 中多个 case 就绪时怎么选
select 不是按代码顺序执行,也不是优先级调度,而是**伪随机选择** —— 这是为了避免饥饿,但也会让行为不可预测,尤其在测试中容易误判。
- 只要至少一个
case就绪(channel 可读/可写),select就会从中挑一个执行,具体选哪个由运行时内部哈希决定,不保证轮询或顺序 -
default是唯一非阻塞分支:只要存在,且所有 channel case 都未就绪,就立刻执行default,不会等待 - 常见陷阱:把
default当成“兜底日志”,结果因伪随机导致某些 case 几乎不被执行,逻辑被绕过 - 实际建议:不要依赖执行顺序;需要确定性时,改用显式判断 + 单个 channel 操作;用
time.After配合select做超时控制更可靠
close(ch) 后继续 send 或 recv 的行为差异
关闭 channel 不是“销毁”,而是发一个“数据已尽”信号。后续操作是否 panic,取决于你是发还是收,以及 channel 是否有缓冲。
- 向已关闭的 channel 发送(
ch ):无论有无缓冲,**一定 panic** —— “send on closed channel” - 从已关闭的无缓冲 channel 接收(
):立即返回对应类型的零值(如 <code>0,"",nil),第二个返回值为false - 从已关闭的有缓冲 channel 接收:先取完缓冲区剩余数据,之后才开始返回零值 +
false - 重复 close:**一定 panic** —— “close of closed channel”,所以关闭前应确保没有其他 goroutine 会再 close 它,通常由 sender 负责关闭
真正难的不是记住这些规则,而是在线上出问题时,能从 pprof 的 goroutine profile 里一眼看出哪个 goroutine 卡在 chan send、哪个在 chan recv,再顺藤摸到没关的 channel 或漏传的 context.CancelFunc。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











