返回归档
🔒Golang

如何避免 channel 被 close 两次

对已经关闭的 channel 再 close 会 panic。select default 分不清「空」和「关」;先查标记再关有 TOCTOU。需要多方都可能关时,用 sync.Once。

文章目录

对已经关掉的 channel 再 close,运行时会 panic。先分清两件不同的事:

  • 关一次:谁有权 close,只让一个人关
  • 读的时候知不知道已经关了:用 v, ok := <-chok == false 表示关了并且排空了

下面三个常见写法,只有中间那个能安全地「关一次」。

方法一:select default 看不出来

select {
case <-ch:
	fmt.Println("channel is closed")
default:
	fmt.Println("channel is not closed")
}

case <-ch 在三种情况下都会成功:

  1. channel 开着,里面刚好有值——读到的是那个值
  2. channel 已经关掉,里面还有残留——读到残留
  3. 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 一起改的话,用方法二。