GO练习题-Goroutinue泄漏 某电商公司的订单服务上线后运行了大约 2 小时OOMOut of Memory被 K8s 杀死进程反复重启。运维同事看到的日志只有一行fatal error: runtime: out of memory。开发同事重启了几次问题依旧。直到有经验的架构师用pprof看了一眼 goroutine 分布才发现每处理一个请求就 leak 一个 goroutine而且这些 goroutine 全部卡在 channel 的 send 操作上永远无法退出。1000 个 QPS × 2 小时 720 万次泄露的 goroutine每个 goroutine 默认 2KB 栈空间再加上分配的堆对象最终把内存吃光了。教训goroutine 泄露是 Go 生产环境中最隐蔽、危害最大的并发问题之一。没有之一。学习目标理解 goroutine 的生命周期谁启动、谁负责停止理解 channel 阻塞如何导致 goroutine 泄露掌握context.WithCancel的正确用法和取消传播链学会用net/http/pprof检测 goroutine 泄露问题描述下面的代码是一个模拟的请求处理函数。每个请求启动一个 worker goroutine 来处理任务并将结果发送到 channel。但这段代码有一个 goroutine 泄露——worker goroutine 在完成工作后试图向resultCh发送结果时如果没有人读取就会永远阻塞。请找出泄露点并修复它。原始代码有 BUGpackage main import ( fmt math/rand time ) func simulateRequest() { resultCh : make(chan string) // 无缓冲 // 启动 worker goroutine 处理请求 go func() { // 模拟耗时处理300~800ms time.Sleep(time.Duration(rand.Intn(500)300) * time.Millisecond) resultCh - fmt.Sprintf(处理完成耗时 %dms, 300rand.Intn(500)) // TODO: 这里会泄露——如果 resultCh 没人读goroutine 永远卡住 }() // TODO: 模拟请求取消或超时的情况比如 500ms 后调用方不再关心结果 // 如果 worker 还没完成它就 leak 了 time.Sleep(500 * time.Millisecond) // 这里不会收到结果因为 goroutine 已经被阻塞在 send 上了 fmt.Println(请求方放弃等待) } func main() { for i : 0; i 5; i { simulateRequest() fmt.Printf(第 %d 个请求处理完毕\n\n, i1) time.Sleep(200 * time.Millisecond) } fmt.Println(所有请求处理完毕) // 运行后观察goroutine 数量只增不减 time.Sleep(2 * time.Second) }预期现象运行后 goroutine 数量持续增长因为这些 goroutine 卡在resultCh - ...上无法退出。修复要求使用context.WithCancel修复泄露问题确保worker goroutine 能感知到取消信号及时退出使用defer cancel()确保清理修复后运行goroutine 数量保持稳定面试追问Q1: 如何用 pprof 检测 goroutine 泄露答// 在 HTTP 服务中启用 pprof import _ net/http/pprof然后通过以下命令检查# 查看 goroutine 数量 curl http://localhost:6060/debug/pprof/goroutine?debug1 # 导出 goroutine profile 到文件 curl http://localhost:6060/debug/pprof/goroutine -o goroutine.prof # 用 pprof 工具分析 go tool pprof goroutine.prof生产环境中定期采集 goroutine 数量并设置告警是必要的。Q2: channel 阻塞的 4 种情况#场景谁阻塞是否泄露1sendCh - val无人接收发送方是发送方 goroutine 永久阻塞2-recvCh无人发送接收方是接收方 goroutine 永久阻塞3双向 channel两端都关闭后继续读写读写方是panic 或永久阻塞4select 中没有 default 分支所有 case 都阻塞select 所在 goroutine是goroutine 永久阻塞Q3: 如何用 context 预防 goroutine 泄露答核心原则——所有从请求衍生出的 goroutine都应该传入同一个 context并在 context 取消时优雅退出。func handler(ctx context.Context) { ctx, cancel : context.WithCancel(ctx) defer cancel() // 关键无论什么路径退出都调用 cancel go func() { defer fmt.Println(goroutine 已退出) select { case -ctx.Done(): return // context 取消优雅退出 case resultCh - doWork(): // 发送成功正常退出 return } }() // ... 处理逻辑 }context 取消传播链用户请求取消 / 超时 → handler 的 context 被 cancel → 所有通过 context.WithCancel/WithTimeout 衍生出的子 context 被 cancel → 所有 select 中 case -ctx.Done() 分支触发 → 所有衍生 goroutine 退出