
Go语言并发编程:锁与通道关闭的陷阱
Go语言中,channel和mutex是处理并发问题的利器,但两者结合使用时,容易出现意想不到的错误,例如本文要讨论的“panic: send on closed channel”问题。即使使用了mutex锁,仍然可能出现此错误。
问题重现
以下代码片段演示了这个问题:
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.TODO())
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, exiting sender")
}
}(i)
}
wg.Wait()
}
问题根源分析
代码中,lock.Lock() 和 lock.Unlock() 保证了close(c)操作的原子性,防止多个goroutine同时关闭通道。然而,select语句的非确定性导致问题。即使通道c已关闭,case c 仍然可能被选中,从而引发panic。这是因为select语句在通道关闭后,会继续尝试发送数据,直到发现通道已关闭。
解决方案
为了避免panic,需要在发送数据前检查通道是否关闭,或者使用上下文机制优雅地关闭goroutine。以下改进后的代码使用上下文机制:
立即学习“go语言免费学习笔记(深入)”;
package main
import (
"context"
"fmt"
"sync"
)
func main() {
c := make(chan int, 10)
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.TODO())
wg.Add(1)
go func() {
defer wg.Done()
cancel() // 先取消上下文
close(c)
}()
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, exiting sender")
}
}(i)
}
wg.Wait()
}
此版本中,我们先取消上下文,再关闭通道。select语句中的case 会优先处理上下文取消信号,避免向已关闭的通道发送数据。 这是一种更健壮的处理并发问题的方案。 直接检查通道是否关闭也是一种可行的方案,但上下文机制通常更优雅且易于维护。










