chan Struct{} 是事件通知最轻量方式,零内存占用,关闭即广播;需用 select+default 避免死锁,禁用重复 close,跨层级取消应优先用 context。

用 chan struct{} 做信号通知最轻量
不需要传递数据,只关心“发生了”这件事时,chan struct{} 是唯一推荐方式。它不占内存(struct{} 零尺寸),关闭 channel 即可广播通知,接收方用 select + default 或 for range 感知。
常见错误是误用 chan bool 或 chan int 传哨兵值,既浪费内存又容易因未读完导致 sender 阻塞。真正只需要“事件到达”语义时,别传任何东西。
- 初始化:
done := make(chan struct{}) - 通知:在需要触发处执行
close(done)(只能关一次) - 等待:
会立即返回(channel 已关闭),或用select { case 非阻塞检查 - 多个 goroutine 同时读
安全,全部会立刻解除阻塞
避免 select 中漏写 default 导致死锁
当 channel 通知只是“可选响应”,而你又不想让 goroutine 卡住时,必须显式加 default 分支。否则如果 channel 尚未关闭或无数据,select 会永久阻塞——尤其在循环中极易演变成资源泄漏。
典型场景:后台任务监听退出信号,但也要定期做点别的事。这时候不能只写 case 。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
select { case → 一旦done没关,永远卡住 - 正确写法:
select { case - 若需超时控制,改用
case 替代default
context.Context 和裸 chan struct{} 怎么选
context.WithCancel 返回的 ctx.Done() 本质也是 chan struct{},但它自带层级取消、超时、截止时间等能力。裸 channel 更适合内部模块间简单解耦;跨函数调用、涉及超时或需要传递取消链时,必须用 context。
混用会导致 cancel 行为不可控:比如你手动 close(ch),但上层 context 还没被 cancel,下游可能收不到一致信号。
- 用裸
chan struct{}:模块内状态同步(如 worker 启动完成、配置重载完成) - 用
context.Context:http handler、数据库查询、长连接心跳等需统一生命周期管理的场景 - 不要把
ctx.Done()和自定义done chan struct{}在同一逻辑里混着等,除非你明确知道谁负责 close 且顺序可靠
关闭已关闭的 chan struct{} 会 panic
这是最容易被忽略的运行时错误:close() 只能调用一次,重复 close 同一个 channel 会直接 panic: “close of closed channel”。不像发送到已关闭 channel 只是 panic,这里连判断都难加——因为 channel 本身不可检测是否已关。
解决思路不是加锁判断,而是从设计上确保只有一个地方有权限关闭。
- 把
chan struct{}包装成结构体字段,并只暴露Close()方法(内部用sync.Once保证幂等) - 或者改用
context.WithCancel(),它的CancelFunc天然幂等 - 绝不要在多个 goroutine 里各自尝试
close(ch),哪怕加了if ch != nil也无效
实际并发通知逻辑里,最麻烦的往往不是怎么发,而是谁来关、什么时候关、关完还能不能读——这些细节不提前想清楚,跑一阵子就出竞态或 panic。