channel不是锁,不能替代sync.Mutex保护临界区;它适用于协程通信、并发控制(如令牌桶)、结果收集(配合sync.WaitGroup)和初始化通知(配合sync.Once),而非原子操作保护。
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 并发执行done ,否则令牌无法回收,后续任务永久阻塞
close(done) 来释放所有令牌——channel 关闭后不能再写入,会 panicfunc 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()}
用 sync.WaitGroup + channel 安全收集结果
单纯用 channel 接收结果时,如果发送端 panic 或提前退出,接收端可能死锁或漏数据;加上 sync.WaitGroup 可明确知道“所有发送者是否已完成”,再关闭 channel,让接收方安全退出。
wg.Done() 标记完成,主 goroutine 调用 wg.Wait() 后再 close(ch)
for range ch,依赖 channel 关闭自动退出,不会漏也不会卡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 zuojiankuohaoph
pcn- id*10 + j
}
}(i)
}
// 启动一个消费者,在所有生产者结束后关闭 channel
go func() {
wg.Wait()
close(ch)
}()
// 安全消费
for val := range ch {
fmt.Println("got:", val)
}}
sync.Once 和 channel 的分工边界
sync.Once 保证某个函数只执行一次,常用于初始化;channel 适合跨 goroutine 通知“初始化已完成”。二者常组合使用,但角色不能颠倒。
channel 实现 “只执行一次” 逻辑——比如靠第一个往 channel 写值来触发初始化,后续 goroutine 等待读,这容易因调度顺序导致重复初始化或死锁sync.Once 包裹初始化逻辑,初始化完成后往 channel 发送信号,其他 goroutine 从该 channel 等待就绪通知chan struct{},零内存开销;别用 chan bool 或 chan int 增加无谓负担真正难处理的是初始化失败后的重试或错误传播——sync.Once 不提供失败重试机制,这时候需要额外状态变量 + channel 配合 error 传递,而不是硬靠 channel 自身语义兜底。