goroutine泄漏因未等待子协程结束导致NumGoroutine持续增长;map并发读写需依场景选sync.RWMutex或sync.Map;向已关闭channel发送会panic,应由发送方唯一关闭。

goroutine 泄漏:忘记等待子协程结束
启动 goroutine 后直接返回,不等它执行完,是初学者最常导致内存和 goroutine 数持续增长的问题。典型场景是循环中启一堆 goroutine 去处理任务,但没用 sync.WaitGroup 或 context 控制生命周期。
常见错误现象:runtime.GOMAXPROCS(0) 不变但 runtime.NumGoroutine() 持续上涨;程序退出后仍有 goroutine 在后台“活着”。
- 必须在启动前调用
wg.Add(1),在 goroutine 内部结尾处调用wg.Done() - 避免在闭包中直接引用循环变量(如
for i := range items { go func() { fmt.Println(i) }() }),应显式传参:go func(idx int) { fmt.Println(idx) }(i) - 若涉及超时或取消,优先用
context.WithTimeout+select配合ctx.Done()退出
map 并发读写 panic:sync.Map 不是万能解药
对原生 map 同时做读和写操作会触发 fatal error: concurrent map read and map write。很多人第一反应是换成 sync.Map,但它只适合「读多写少 + 键值类型简单」的场景,且 API 设计反直觉(比如没有 Len()、遍历需用 Range())。
使用场景判断比盲目替换更重要:
立即学习“go语言免费学习笔记(深入)”;
- 若写操作频繁(如每秒数百次更新),
sync.RWMutex+ 普通map通常性能更好、语义更清晰 -
sync.Map的LoadOrStore返回值是(value, loaded bool),容易忽略loaded导致逻辑错判 - 不要在
sync.Map.Range回调里调用m.Delete或m.Store—— 它不保证遍历期间 map 状态一致
channel 关闭后继续 send:panic: send on closed channel
向已关闭的 channel 发送数据会立即 panic,但接收仍可继续直到缓冲耗尽。初学者常在多个 goroutine 中重复关闭同一 channel,或在不确定是否关闭时贸然 close(ch)。
- channel 应由发送方(或唯一拥有发送权的一方)负责关闭;接收方绝不调用
close() - 用
select+default避免阻塞发送:select { case ch - 检查 channel 是否已关闭只能靠接收侧的第二个返回值:
v, ok := ;ok == false表示 channel 已关且无剩余数据 - 不要依赖
len(ch) == 0判断是否可安全发送 —— 它只反映当前缓冲长度,和关闭状态无关
WaitGroup 使用时机错位:Add 在 Go 之后调用
sync.WaitGroup 的 Add() 必须在 go 语句之前调用,否则极大概率出现 panic: sync: negative WaitGroup counter 或静默失效。这是因为 goroutine 启动后可能瞬间执行完毕并调用 Done(),而主线程还没来得及 Add()。
一个典型反模式:
go func() {
wg.Add(1) // ❌ 危险!Add 在 goroutine 内部
defer wg.Done()
// ...
}()
wg.Wait() // 此时 wg.counter 可能还是 0
- 正确顺序永远是:
wg.Add(1)→go func() { defer wg.Done(); ... }() - 如果要启动 N 个 goroutine,
wg.Add(N)必须在所有go之前一次性完成,不能拆到循环体内每个go前面单独加 - 不要在 goroutine 中多次调用
wg.Done(),也不要在未Add()时调用Done()
并发真正的复杂点不在语法,而在状态归属和所有权边界——哪个 goroutine 负责关 channel,谁持有 map 的写权限,WaitGroup 计数器该由谁增减。这些不是靠记住规则就能避开的,得在每次写 go 前停半秒,问自己:它什么时候停?资源谁清理?别人会不会同时碰同一块内存?











