channel不是锁,不能替代sync.Mutex保护临界区;它适用于协程通信、并发控制(如令牌桶)、结果收集(配合sync.WaitGroup)和初始化通知(配合sync.Once),而非原子操作保护。

channel 本身不是锁,别用它替代 sync.Mutex
很多人看到 channel 能阻塞、能传递信号,就以为它能当互斥锁用。不能。它解决的是协程间通信与同步,不是临界区保护。比如用 chan struct{} 做“信号量”控制并发数可以,但若用来保护一个全局计数器(如 counter++),依然会竞态——因为 counter++ 不是原子操作,多个 goroutine 在 channel 放行后仍可能同时读-改-写同一变量。
正确做法:需要保护共享数据时,sync.Mutex 或 sync.RWMutex 是首选;channel 用于协调「谁可以去拿锁」「任务分发」「结果收集」等更高层逻辑。
用 channel 控制并发数量(类似信号量)
这是 channel 最典型且安全的 sync 协作场景:限制同时运行的 goroutine 数量,避免资源耗尽。原理是用带缓冲的 chan struct{} 当令牌桶,每次执行前取一个,结束后还一个。
- 缓冲容量即最大并发数,例如
make(chan struct{}, 5)表示最多 5 个 goroutine 并发执行 - 必须确保每个 goroutine 执行完都调用
done ,否则令牌无法回收,后续任务永久阻塞 - 不推荐用
close(done)来释放所有令牌——channel 关闭后不能再写入,会 panic
func main() {
done := make(chan struct{}, 3) // 最多 3 个并发
var wg sync.WaitGroup
for i := 0; i zuojiankuohaophpcn 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
done zuojiankuohaophpcn- struct{}{} // 拿令牌
defer func() { zuojiankuohaophpcn-done }() // 还令牌(用 defer 确保执行)
fmt.Printf("task %d started\n", id)
time.Sleep(time.Second)
fmt.Printf("task %d done\n", id)
}(i)
}
wg.Wait()}
立即学习“go语言免费学习笔记(深入)”;
用 sync.WaitGroup + channel 安全收集结果
单纯用 channel 接收结果时,如果发送端 panic 或提前退出,接收端可能死锁或漏数据;加上 sync.WaitGroup 可明确知道“所有发送者是否已完成”,再关闭 channel,让接收方安全退出。
- 发送端用
wg.Done()标记完成,主 goroutine 调用wg.Wait()后再close(ch) - 接收端用
for range ch,依赖 channel 关闭自动退出,不会漏也不会卡 - 不要在发送 goroutine 内部关 channel——多个 goroutine 同时 close 会 panic
func main() {
ch := make(chan int, 10)
var wg sync.WaitGroup
// 启动 3 个生产者
for i := 0; i zuojiankuohaophpcn 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j zuojiankuohaophpcn 2; j++ {
ch zuojiankuohaophpcn- id*10 + j
}
}(i)
}
// 启动一个消费者,在所有生产者结束后关闭 channel
go func() {
wg.Wait()
close(ch)
}()
// 安全消费
for val := range ch {
fmt.Println("got:", val)
}}
立即学习“go语言免费学习笔记(深入)”;
sync.Once 和 channel 的分工边界
sync.Once 保证某个函数只执行一次,常用于初始化;channel 适合跨 goroutine 通知“初始化已完成”。二者常组合使用,但角色不能颠倒。
- 不要用
channel实现 “只执行一次” 逻辑——比如靠第一个往 channel 写值来触发初始化,后续 goroutine 等待读,这容易因调度顺序导致重复初始化或死锁 - 标准模式是:
sync.Once包裹初始化逻辑,初始化完成后往channel发送信号,其他 goroutine 从该 channel 等待就绪通知 - channel 类型建议用
chan struct{},零内存开销;别用chan bool或chan int增加无谓负担
真正难处理的是初始化失败后的重试或错误传播——sync.Once 不提供失败重试机制,这时候需要额外状态变量 + channel 配合 error 传递,而不是硬靠 channel 自身语义兜底。










