用 make(chan struct{}, N) 限制并发协程数最直接:缓冲容量 N 即最大并发数,发送 struct{}{} 获取许可,接收释放;误用无缓冲 channel 会导致串行化,struct{} 零开销且语义清晰。

用 make(chan struct{}, N) 限制并发协程数最直接
Go 中没有内置“线程池”概念,但用带缓冲的 chan struct{} 模拟信号量是最轻量、最符合 Go 风格的做法。缓冲容量 N 就是最大并发数,每次启动新协程前先向 channel 发送一个空结构体(struct{}{}),执行完再接收一次释放信号。
常见错误是误用无缓冲 channel(make(chan struct{}))——它会导致所有协程在发送时立刻阻塞,无法实现“控制并发数”,只实现“串行化”。也有人用 chan int 或 chan bool,没必要,struct{} 零内存占用,语义也最清晰。
示例关键片段:
defer
- 用
defer func(){ (推荐) - 或显式写
defer func() { ,避免和外部变量混淆 - 不要依赖
recover来兜底——channel 阻塞不会 panic,只会卡住
用 sync.WaitGroup 配合 channel 更稳妥
单纯靠 channel 信号量能控并发,但没法等全部任务结束。实际场景中往往既要限流又要等待完成,这时必须加 sync.WaitGroup。
典型组合模式:
- 先
wg.Add(1),再启动 goroutine - goroutine 内部用
defer wg.Done()标记完成 -
sem只负责准入控制,wg负责生命周期同步 - 主 goroutine 调用
wg.Wait()等全部结束,而不是靠 sleep 或计数器
漏掉 wg.Add 或 wg.Done 会导致程序提前退出或永远 hang 住,这类 bug 很难复现,建议用 go vet 检查 WaitGroup 使用是否匹配。
别忽略 context 取消和超时场景
真实服务中,任务可能被取消或超时,此时仅靠 sem 释放不够——如果某个 doWork 卡死或慢查询,它占着信号量不放,后续任务全被堵住。
必须结合 context.WithTimeout 或 context.WithCancel:
- 在 goroutine 内部监听
ctx.Done(),及时退出 - 退出前仍要保证
执行,否则信号量泄漏,后续所有任务都会被 block - 可用
select+default避免阻塞释放:比如select { case 不安全,应始终确保释放,哪怕用close(sem)也不如正常接收可靠
最简健壮模式是:把 放进 defer,并在主逻辑里用 select 监听 ctx.Done() 和工作完成,确保无论哪种路径退出,defer 都能执行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











