什么情况下关闭 channel 会造成 panic?有没有必要关闭 channel?如何判断 channel 是否关闭?如何优雅地关闭 channel?这些你都知道吗?(不要告诉我你只会回答最后一个问题!)看到这一溜烟的问题,不知道你会不会不禁感叹,究竟哪个天杀的总说 go channel “哲学”“优雅”的?也许 Rob Pike (go语言之父)会说:嗯,当然“哲学”“优雅”,只是需要你注意的问题有点多……

什么情况下关闭 channel 会造成 panic ?

  1. // 1.未初始化时关闭
  2. func TestCloseNilChan(t *testing.T) {
  3. var errCh chan error
  4. close(errCh)
  5. // Output:
  6. // panic: close of nil channel
  7. }
  8. // 2.重复关闭
  9. func TestRepeatClosingChan(t *testing.T) {
  10. errCh := make(chan error)
  11. var wg sync.WaitGroup
  12. wg.Add(1)
  13. go func() {
  14. defer wg.Done()
  15. close(errCh)
  16. close(errCh)
  17. }()
  18. wg.Wait()
  19. // Output:
  20. // panic: close of closed channel
  21. }
  22. // 3.关闭后发送
  23. func TestSendOnClosingChan(t *testing.T) {
  24. errCh := make(chan error)
  25. var wg sync.WaitGroup
  26. wg.Add(1)
  27. go func() {
  28. defer wg.Done()
  29. close(errCh)
  30. errCh <- errors.New("chan error")
  31. }()
  32. wg.Wait()
  33. // Output:
  34. // panic: send on closed channel
  35. }
  36. // 4.发送时关闭
  37. func TestCloseOnSendingToChan(t *testing.T) {
  38. errCh := make(chan error)
  39. var wg sync.WaitGroup
  40. wg.Add(1)
  41. go func() {
  42. defer wg.Done()
  43. defer close(errCh)
  44. go func() {
  45. errCh <- errors.New("chan error") // 由于 chan 没有缓冲队列,代码会一直在此处阻塞
  46. }()
  47. time.Sleep(time.Second) // 等待向 errCh 发送数据
  48. }()
  49. wg.Wait()
  50. // Output:
  51. // panic: send on closed channel
  52. }

复制代码
综上,我们可以总结出如下知识点:
【知识点】在下述 4 种情况关闭 channel 会引发 panic:未初始化时关闭、重复关闭、关闭后发送、发送时关闭。
另外,从 golang 的报错中我们可以知道,golang 认为第3种和第4种情况属于一种情况。
通过观察上述代码,为避免在使用 channel 时遇到重复关闭、关闭后发送的问题,我想我们可以总结出以下两点规律:

  • 应该只在发送端关闭 channel。(防止关闭后继续发送)
  • 存在多个发送者时不要关闭发送者 channel,而是使用专门的 stop channel。(因为多个发送者都在发送,且不可能同时关闭多个发送者,否则会造成重复关闭。发送者和接收者多对一时,接收者关闭 stop channel;多对多时,由任意一方关闭 stop channel,双方监听 stop channel 终止后及时停止发送和接收)

这两点规律被称为“channel 关闭守则”。
既然关闭 channel 这么麻烦,那么我们有没有必要关闭 channel 呢?不关闭又如何?

有没有必要关闭 channel?不关闭又如何?

我们考虑以下两种情况:
情况一:channel 的发送次数等于接收次数

  1. func TestIsCloseChannelNecessary_on_equal(t *testing.T) {
  2. fmt.Println("NumGoroutine:", runtime.NumGoroutine())
  3. ich := make(chan int)
  4. // sender
  5. go func() {
  6. for i := 0; i < 3; i++ {
  7. ich <- i
  8. }
  9. }()
  10. // receiver
  11. go func() {
  12. for i := 0; i < 3; i++ {
  13. fmt.Println(<-ich)
  14. }
  15. }()
  16. time.Sleep(time.Second)
  17. fmt.Println("NumGoroutine:", runtime.NumGoroutine())
  18. // Output:
  19. // NumGoroutine: 2
  20. // 0
  21. // 1
  22. // 2
  23. // NumGoroutine: 2
  24. }

channel 的发送次数等于接收次数时,发送者 go routine 和接收者 go routine 分别都会在发送或接收结束时结束各自的 go routine。而上述代码中的 ich 会由于没有代码使用被垃圾收集器回收。因此这种情况下,不关闭 channel,没有任何副作用。
情况二:channel 的发送次数大于/小于接收次数

  1. func TestIsCloseChannelNecessary_on_less_sender(t *testing.T) {
  2. fmt.Println("NumGoroutine:", runtime.NumGoroutine())
  3. ich := make(chan int)
  4. // sender
  5. go func() {
  6. for i := 0; i < 2; i++ {
  7. ich <- i
  8. }
  9. }()
  10. // receiver
  11. go func() {
  12. for i := 0; i < 3; i++ {
  13. fmt.Println(<-ich)
  14. }
  15. }()
  16. time.Sleep(time.Second)
  17. fmt.Println("NumGoroutine:", runtime.NumGoroutine())
  18. // Output:
  19. // NumGoroutine: 2
  20. // 0
  21. // 1
  22. // NumGoroutine: 3
  23. }

以上述代码为例,channel 的发送次数小于接收次数时,接收者 go routine 由于等待发送者发送一直阻塞。因此接收者 go routine 一直未退出,ich 也由于一直被接收者使用无法被垃圾回收。未退出的 go routine 和未被回收的 channel 都造成了内存泄漏的问题。
因此,在发送者与接收者一对一的情况下,只要我们确保发送者或接收者不会阻塞,不关闭 channel 是可行的。在我们无法准确判断 channel 的发送次数和接收次数时,我们应该在合适的时机关闭 channel。那么如何判断 channel 是否关闭呢?

如何判断 channel 是否关闭?

【知识点】go channel 关闭后,读取该 channel 永远不会阻塞,且只会输出对应类型的零值。

  1. func TestReadFromClosedChan(t *testing.T) {
  2. var errCh = make(chan error)
  3. go func() {
  4. defer close(errCh)
  5. errCh <- errors.New("chan error")
  6. }()
  7. go func() {
  8. for i := 0; i < 3; i++ {
  9. fmt.Println(i, <-errCh)
  10. }
  11. }()
  12. time.Sleep(time.Second)
  13. // Output:
  14. // 0 chan error
  15. // 1 <nil>
  16. // 2 <nil>
  17. }

以上述代码为例,nil 可能也是需要 channel传输的值之一,通常我们无法通过判断是否为类型的零值确定 channel 是否关闭。所以为了避免输出无意义的值,我们需要一种合理的方式判断 channel 是否关闭。golang 官方为我们提供了两种方式。
解决方案一:使用 channel 的多重返回值(如 err, ok := <-errCh )

  1. func TestReadFromClosedChan2(t *testing.T) {
  2. var errCh = make(chan error)
  3. go func() {
  4. defer close(errCh)
  5. errCh <- errors.New("chan error")
  6. }()
  7. go func() {
  8. for i := 0; i < 3; i++ {
  9. if err, ok := <-errCh; ok {
  10. fmt.Println(i, err)
  11. }
  12. }
  13. }()
  14. time.Sleep(time.Second)
  15. // Output:
  16. // 0 chan error
  17. }

err, ok := <-errCh 的第二个返回值 ok 表示 errCh 是否已经关闭。如果已关闭,则返回 false。
解决方案二:使用 for range 简化语法

  1. func TestReadFromClosedChan(t *testing.T) {
  2. var errCh = make(chan error)
  3. go func() {
  4. defer close(errCh)
  5. errCh <- errors.New("chan error")
  6. }()
  7. go func() {
  8. i := 0
  9. for err := range errCh {
  10. fmt.Println(i, err)
  11. i++
  12. }
  13. }()
  14. time.Sleep(time.Second)
  15. // Output:
  16. // 0 chan error
  17. }

for range 语法会自动判断 channel 是否结束,如果结束则自动退出 for 循环。

如何优雅地关闭 channel ?

golang 允许我们使用 <- 控制 channel 发送方向,防止我们在错误的时候关闭 channel。

  1. func TestOneSenderOneReceiver(t *testing.T) {
  2. ich := make(chan int)
  3. go sender(ich)
  4. go receiver(ich)
  5. }
  6. func sender(ich chan<- int) {
  7. for i := 0; i < 100; i++ {
  8. ich <- i
  9. }
  10. }
  11. func receiver(ich <-chan int) {
  12. fmt.Println(<-ich)
  13. close(ich) // 此处代码会在编译期报错
  14. }

使用这种方法时,由于 close() 函数只能接受 chan<- T 类型的 channel,如果我们尝试在接收方关闭 channel,编译器会报错,所以我们可以在编译期提前发现错误。
除此之外,我们也可以使用如下的结构体(抄自go101《如何优雅地关闭 go channels》,做了一点修改,链接为此文的中文翻译):

  1. type Channel struct {
  2. C chan interface{}
  3. closed bool
  4. mut sync.Mutex
  5. }
  6. func NewChannel() *Channel {
  7. return NewChannelSize(0)
  8. }
  9. func NewChannelSize(size int) *Channel {
  10. return &Channel{
  11. C: make(chan interface{}, size),
  12. closed: false,
  13. mut: sync.Mutex{},
  14. }
  15. }
  16. func (c *Channel) Close() {
  17. c.mut.Lock()
  18. defer c.mut.Unlock()
  19. if !c.closed {
  20. close(c.C)
  21. c.closed = true
  22. }
  23. }
  24. func (c *Channel) IsClosed() bool {
  25. c.mut.Lock()
  26. defer c.mut.Unlock()
  27. return c.closed
  28. }
  29. func TestChannel(t *testing.T) {
  30. ch := NewChannel()
  31. println(ch.IsClosed())
  32. ch.Close()
  33. ch.Close()
  34. println(ch.IsClosed())
  35. }

该方案可以解决重复关闭锁的问题以及锁是否关闭的问题。通过 Channel.IsClosed() 判断是否关闭 channel ,又可以安全地发送和接收。当然我们也可以把 sync.Mutex 换成 sync.Once,来只让 channel 关闭一次。具体可以参考《如何优雅地关闭 go channels》。

有时候我们的代码已经使用了原生的 chan,或者我们不想使用单独的数据结构,也可以使用下述的几种方案。通常情况下,我们只会遇到四种需要关闭 channel 的情况(以下内容是我对《如何优雅地关闭 go channels》中方法的总结):

  • 一个发送者,一个接收者:发送者关闭 channel,接收者使用 select 或 for range 判断 channel 是否关闭。
  • 一个发送者,多个接收者:发送者关闭 channel,同上。
  • 多个发送者,一个接收者:接收者接收完毕后,使用专用的 stop channel 关闭;发送者使用 select 监听 stop channel 是否关闭。
  • 多个发送者,多个接收者:任意一方使用专用的 stop channel 关闭;发送者、接收者都使用 select 监听 stop channel 是否关闭。

因此我们只需要熟记面对这四种情况时如何关闭 channel 即可。为避免单纯地抄袭,具体的代码实现可以去参考《如何优雅地关闭 go channels》这篇文章(划到中间位置,找“保持channel closing principle的优雅方案”关键字即可)。