必须用 chan struct{} 而不是 chan bool 或 chan int,因其零内存开销、语义明确(纯信号不传数据)、编译器专门优化,且避免真假歧义或数值误用,是 goroutine 优雅退出和同步通知的最佳实践。

为什么用 chan struct{} 而不是 chan bool 或 chan int
因为 struct{} 零内存占用、语义清晰,且 Go 编译器对空结构体通道有专门优化。用 chan bool 不仅多占 1 字节(实际可能因对齐更大),还容易让人误以为要传递真假逻辑;chan int 更是完全偏离“信号通知”本意。空结构体通道唯一作用就是阻塞/唤醒,不传数据——这是优雅退出的前提。
select + case 是协程退出的黄金组合
每个监听退出信号的 goroutine 必须在循环中用 select 非阻塞或带超时地检查 done 通道,不能只靠一次读取就退出。常见错误是写成:
if _, ok := <-done; !ok { return }这会消费掉信号,其他协程收不到;或者漏掉 default 导致主逻辑被阻塞。正确写法是:
- 始终把
放在 <code>select的一个case中 - 需要持续运行的任务,用
for包裹select - 避免在
case中做耗时操作,否则会延迟响应退出 - 如果需清理资源,放在
select外的defer或return前
close(done) 是通知源头,但只能调用一次
关闭 done 通道后,所有阻塞在 的 goroutine 会立即解除阻塞并收到零值(即 <code>struct{}{})。关键点:
立即学习“go语言免费学习笔记(深入)”;
- 必须由发起方(如主协程或 manager)调用
close(done),不能重复 close,否则 panic:panic: close of closed channel - 不要用
done 发送,那只能唤醒一个协程;<code>close才能广播 - 如果
done是函数参数传入,确保调用者明确知道它会被 close —— 否则可能提前关闭导致误唤醒 - 若需多次通知(如暂停/恢复),得换用其他机制,
chan struct{}只适合“一次性终止”
典型函数结构:带 cleanup 的可取消 worker
一个实用模板长这样,重点看模式而非语法细节:
func runWorker(ctx context.Context, done <-chan struct{}) {
defer fmt.Println("worker exited")
for {
select {
case <-done:
return
default:
// 模拟工作
time.Sleep(100 * time.Millisecond)
}
}
}注意:done 类型是 (只接收),防止 worker 错误关闭或发送;真正 close 的地方在调用侧,比如:
done := make(chan struct{})
go runWorker(context.Background(), done)
// ... 一段时间后
close(done) // 这里触发全部监听者退出复杂点在于:多个子协程共享同一个 done 时,它们各自 clean up 的时机可能不同步;如果有依赖顺序(比如 A 必须等 B 完全退出后再关 DB 连接),就得额外加同步原语,chan struct{} 本身不保证退出顺序。


















