
Go 语言加锁代码偶尔 panic 的原因分析:send on closed channel
在 Go 语言并发编程中,sync.Mutex 锁常用于保护共享资源,确保线程安全。然而,即使使用了锁,仍然可能出现 send on closed channel 的 panic 错误。本文将分析其原因。
问题代码及分析
以下代码片段演示了该问题:
package main
import (
"context"
"fmt"
"sync"
)
var lock sync.Mutex
func main() {
c := make(chan int, 10)
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
lock.Lock()
cancel()
close(c)
lock.Unlock()
}()
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
select {
case c <- i:
fmt.Printf("Sent: %d\n", i)
case <-ctx.Done():
fmt.Println("Context cancelled")
}
}(i)
}
wg.Wait()
}
这段代码使用了一个带缓冲区的通道 c 和一个互斥锁 lock。 lock 用于保护 close(c) 操作的原子性。然而,select 语句的非确定性导致问题。
问题根源
Go 语言的 select 语句具有非确定性:如果多个 case 都可执行,Go 运行时会随机选择一个执行。
关键在于以下两点:
-
select的随机性:select语句中,case c 和case 都有可能被选中。 -
关闭通道后的发送: 向已关闭的通道发送数据会引发
send on closed channelpanic。
即使 cancel() 函数在 lock 保护下调用,也无法保证所有 goroutine 都能及时感知到 ctx.Done() 并退出 select 语句中的发送操作。如果在 close(c) 后,某个 goroutine 仍然随机选择了 case c ,就会发生 panic。
解决方法
避免此类问题的关键在于确保在关闭通道前,所有向该通道发送数据的 goroutine 都已完成或停止尝试发送数据。 可以使用以下方法:
-
使用
WaitGroup协调 goroutine: 确保所有发送数据的goroutine都已完成,然后再关闭通道。 -
在发送前检查通道是否关闭: 在
select语句中添加一个defaultcase,或者在发送前显式检查通道是否关闭 (会阻塞直到有数据或通道关闭,可以利用这个特性)。
改进后的代码示例: (使用 WaitGroup 协调)
// ... (previous code) ... wg.Wait() // Wait for all senders to finish lock.Lock() close(c) lock.Unlock() // ... (rest of the code) ...
通过合理的并发控制和对 select 语句行为的理解,可以有效避免 send on closed channel 的 panic 错误,即使在使用了锁的情况下。










