waitgroup 的 add 必须在启动 goroutine 之前调用,否则 done 可能先执行导致计数器为负而 panic;once.do 不接受带参函数,需用闭包或全局配置绕过;sync.map 在写多或 key 频繁变更时比 map+mutex 更慢;混用 waitgroup 和 once 时,务必确保所有 goroutine 分支都执行 wg.done()。

WaitGroup 什么时候该 Add,Add 太早或太晚会怎样
WaitGroup 不是自动发现 goroutine 的魔法,Add 必须在启动 goroutine 之前调用,否则极大概率触发 panic:panic: sync: negative WaitGroup counter。这是因为 Done 可能在 Add 之前执行(比如 goroutine 瞬间完成),导致计数器变负。
常见错误写法:
go func() {
wg.Done() // 错!wg.Add 还没调用
}()
正确做法只有一种:先 Add,再启 goroutine:
-
wg.Add(1)必须出现在go func() { ... wg.Done() }()之前,且不能被条件跳过 - 如果启多个 goroutine,
Add(n)要一次性加总数,不要循环里每次Add(1)—— 除非你确保循环不会并发执行 - 不支持“动态增减”:Add 后不能再 Add(除非你明确知道当前无 goroutine 正在调用
Done)
Once.Do 为什么不能传带参函数,怎么绕过
sync.Once.Do 只接受 func() 类型,传入带参数的函数会编译失败:cannot use func(x int) {} (type func(int)) as type func() in argument to once.Do。这不是设计缺陷,而是为了强制“初始化逻辑无外部依赖”,避免隐式状态泄漏。
立即学习“go语言免费学习笔记(深入)”;
实际中你要初始化一个带配置的 logger 或 DB 连接,不能硬塞参数进去。可行解法只有两个:
- 闭包捕获变量(最常用):
once.Do(func() { initDB(dsn) }),其中dsn是外部已确定的变量 - 用指针或全局配置结构体承载参数,
Do内部读取 —— 但要注意该结构体本身是否并发安全 - 别试图用
func() interface{}强转,Go 类型系统会拦住你
注意:Do 内部 panic 会导致 Once 永久标记为“已完成”,后续调用直接返回,不会重试 —— 所以初始化函数里要自己 recover 异常。
sync.Map 在什么场景下比 map + mutex 更慢
sync.Map 不是通用替代品。它在**读多写少、键生命周期长、且 key 类型固定**时才可能赢;一旦写操作占比超过 ~15%,或者频繁增删 key,它往往比 map 加 sync.RWMutex 更慢,甚至高几倍。
根本原因在于:sync.Map 用两层 map(read + dirty)+ 原子操作 + 额外指针跳转,写操作要升级、复制、清理,成本远高于一次 mutex lock + 直接赋值。
- 高频写(如计数器每秒更新万次):用
sync.Map反而增加 GC 压力和 cache miss - key 类型不是
string或int(比如 struct):sync.Map的Load/Store接口要求 interface{},会逃逸 + 额外类型断言 - 需要遍历或 len():
sync.Map的Range是快照语义,且无法获取准确长度;而普通 map + RWMutex 可以安全读 len 或 range
WaitGroup 和 Once 混用时最容易漏掉的释放点
WaitGroup 和 Once 经常一起出现在初始化模块中,比如“等所有 worker 启动完,再统一触发 start”。这时候最容易漏的是:WaitGroup 的 Done 没有覆盖所有退出路径。
典型坑:
- goroutine 内部有 error 分支,只在 success 路径调
wg.Done(),panic 或 return 早了就永远卡住Wait - 用 defer
wg.Done()但 defer 没生效(比如函数直接 return,defer 还没注册)—— 实际上 defer 是安全的,但很多人误以为它“一定执行”,忘了 defer 只对当前 goroutine 生效,而 WaitGroup 是跨 goroutine 的 -
Once.Do里启了 goroutine 但没关联 WaitGroup:Once 只保证执行一次,不保证执行完,更不保证内部 goroutine 结束
真正可靠的模式只有一种:每个 goroutine 入口第一行是 wg.Add(1)(主 goroutine),然后启子 goroutine,在子 goroutine 最末尾(或 defer)调 wg.Done(),且所有分支都必须走到那里。










