对已经关掉的 channel 再 close,运行时会 panic。先分清两件不同的事:
- 关一次:谁有权
close,只让一个人关 - 读的时候知不知道已经关了:用
v, ok := <-ch,ok == false表示关了并且排空了
下面三个常见写法,只有中间那个能安全地「关一次」。
方法一:select default 看不出来
select {
case <-ch:
fmt.Println("channel is closed")
default:
fmt.Println("channel is not closed")
}
case <-ch 在三种情况下都会成功:
- channel 开着,里面刚好有值——读到的是那个值
- channel 已经关掉,里面还有残留——读到残留
- channel 已经关掉,并且空了——读到零值
只有「开着而且空」才会走 default。所以这既不能当「是否已关闭」的检测,更不能当「能不能 close」的依据。有人往里面发过 struct{}{},你会误判成已关闭。
要检测关闭,用双返回值:
v, ok := <-ch
if !ok {
// 已关闭,并且没有未读完的值
}
方法二:sync.Once
type MyChan struct {
ch chan struct{}
once sync.Once
}
func (m *MyChan) Close() {
m.once.Do(func() {
close(m.ch)
})
}
Do 保证函数体只跑一次,并发调用也只关一次。多个出口都可能结束、又没有单一 owner 时,用这个。
更好的设计仍是:约定只有 sender 关。Once 是「关口不止一个」时的补丁,不是先上的架构。
方法三:先看标记再关,有空窗
func (m *MyChan) Close() {
if m.isclosed() { // 内部加锁、读 closed、解锁
return
}
m.Lock()
defer m.Unlock()
m.closed = true
close(m.ch)
}
isclosed 返回之后锁已经放下。两个 goroutine 都可以看到 closed == false,再分别走进 close。第二次就会 panic。
检查和关闭必须在同一把锁里:
func (m *MyChan) Close() {
m.Lock()
defer m.Unlock()
if m.closed {
return
}
m.closed = true
close(m.ch)
}
这样正确,但和 Once 比只是多写了锁。没有额外状态要跟 closed 一起改的话,用方法二。