应使用 channel 配合 select + context 而非 sync.Cond,因后者违背 Go“通过通信共享内存”哲学,且易导致死锁、无超时机制、调度不可控;channel + select 可自然支持超时、取消与任务分发。

用 channel 配合 select + context,而不是 sync.Cond —— 后者在 Go 并发模型里属于“能跑但不推荐”的遗留方案。
为什么不用 sync.Cond 做协程协同通信
它确实能阻塞/唤醒协程,但违背 Go “通过通信共享内存” 的设计哲学。实际项目中容易踩的坑包括:
-
sync.Cond必须搭配一个外部锁(L Locker),而锁的持有范围、释放时机稍有不慎就会导致死锁或竞态 - 没有内置超时或取消机制,必须手动包装
time.After或context.WithTimeout,代码冗长且易错 - 多个
Wait()协程被Broadcast()唤醒后,无法控制谁先抢到任务,缺乏调度语义 - Go 官方文档和
go vet都会隐式提示:优先用 channel 实现同步
channel + select 实现带超时的任务分发
这是最贴近真实业务的协同模式:一个生产者往 chan Task 写任务,多个消费者用 select 等待,同时支持退出信号和超时兜底。
关键点:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 通道类型建议定义为
chan Task,其中Task是带context.Context字段的结构体,便于下游透传取消信号 -
select中必须包含分支,否则协程无法响应父级上下文取消 - 避免无缓冲通道阻塞生产者;若任务量大,用带缓冲通道(如
make(chan Task, 100))并配合背压策略 - 不要在
select里写default:空分支——这会让协程忙等,消耗 CPU
示例片段:
func worker(id int, tasks <-chan Task, done chan<- struct{}) {
defer func() { done <- struct{}{} }()
for {
select {
case task, ok := <-tasks:
if !ok {
return
}
task.Handler(task)
case <-task.Ctx.Done(): // 注意:这里 task.Ctx 来自每个任务自身
return
case <-time.After(3 * time.Second): // 防止单个任务卡死
continue
}
}
}如何让多个协程安全共享状态而不加锁
直接暴露全局变量或结构体字段给多个 goroutine 读写,几乎必然触发 data race。正确做法是:
- 把状态封装进一个专用的“状态协程”,它独占读写,其他协程只通过
chan向它发指令(如GetCount、Inc) - 使用
sync/atomic处理简单整型计数器(如请求总数、成功数),避免锁开销 - 对 map 类型状态,要么用
sync.Map(适合读多写少),要么彻底放弃并发读写,改用 channel 转发操作请求 - 切忌在
select的每个case里嵌套锁 —— 这极易造成锁粒度失控和死锁链
协程退出时资源清理常被忽略的点
很多人只记得 close(ch) 或 cancel(),但以下三点更隐蔽也更关键:
- 如果协程里启了子协程(比如日志上报、指标打点),父协程退出前必须确保子协程已结束,否则形成 goroutine 泄漏
- 文件句柄、数据库连接、HTTP client transport 等资源,不能依赖 GC 回收,必须显式调用
Close()或Stop() - 用
defer注册清理函数时,注意它只在当前 goroutine 返回时执行 —— 若协程因ctx.Done()提前退出,defer仍会运行,但要确认清理逻辑本身不阻塞
最稳妥的退出模式是:所有协程监听同一个 ctx.Done(),收到信号后停止接收新任务、处理完手头任务、执行清理、关闭输出通道、发送完成信号。

















