Go channel FAQ

close channel 設計理念

func closechan(c *hchan) {
   // 關閉一個 nil channel, 拋出 panic
   if c == nil {
     panic(plainError("close of nil channel"))
   }
}

爲什麼關閉一個已經關閉的 channelpanic ?

官方這樣設計的初衷,應該是希望開發者不要依賴於 close 函數,而是要求開發者通過合理設計 goroutine + channel 工作流來提高程序的健壯性。

如何檢測 channel 關閉

package main

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan int)

   go func() {
      if val, ok := <-ch; !ok {
         // channel 已關閉
         fmt.Println("channel closed")
      } else {
         fmt.Printf("val = %d", val)
      }
   }()

   ch <- 1024
   close(ch)

   time.Sleep(time.Second)
}

如何實現健壯的 channel close 方法

關閉一個已經關閉的 channel 會 panic, 實現一個方法,可以讓調用方無需考慮邊界情況,直接調用即可。

下面的代碼只是作爲技術解決方案探究,沒有任何實際意義 (不要應用在你的任何業務代碼中)。

1. recover

通過 recover 函數捕獲 panic, 可以保證關閉一個已經關閉的 channel 報錯不會導致程序終止。

package main

import (
 "fmt"
)

func main() {
 defer func() {
  if err := recover(); err != nil {
   fmt.Printf("recover err: %v\n", err)
  }
 }()

 ch := make(chan int)

 close(ch)
 // 關閉一個已經關閉的 channel
 close(ch)
}

2. sync.Once

通過 sync.Once 方法保證 close(channel) 只會被調用一次。

package main

import (
 "sync"
)

type myChan struct {
 ch   chan int
 once sync.Once
}

func (c *myChan) close() {
 c.once.Do(func() {
  close(c.ch)
 })
}

func main() {
 ch := &myChan{
  ch: make(chan int),
 }

 ch.close()
 // 關閉一個已經關閉的 channel
 ch.close()
}

3. atomic.CAS

通過 atomic.CAS 方法保證 close(channel) 只會被調用一次。

package main

import "sync/atomic"

type myChan struct {
 ch     chan int
 closed int32
}

func (c *myChan) close() {
 if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
  close(c.ch)
 }
}

func main() {
 ch := &myChan{
  ch: make(chan int),
 }
 ch.close()
 // 關閉一個已經關閉的 channel
 ch.close()
}

4. context.Context

通過 context.Context 保證 close(channel) 的操作順序同步。

package main

import (
 "context"
)

type myChan struct {
 ch     chan int
 ctx    context.Context
 cancel context.CancelFunc
}

func (c *myChan) close() {
 select {
 case <-c.ctx.Done():
  return
 default:
  close(c.ch)
  // 事件同步
  c.cancel()
 }
}

func main() {
 ctx, cancel := context.WithCancel(context.Background())

 ch := &myChan{
  ch:     make(chan int),
  ctx:    ctx,
  cancel: cancel,
 }

 ch.close()
 // 關閉一個已經關閉的 channel
 ch.close()
}

channel 最佳實踐

  1. channel 類型作爲參數時,指定操作類型 (讀 / 寫)

  2. 使用 select + default 處理多個 channel 輪詢場景

  3. 永遠不要在讀取方向關閉 channel, 只在寫入端關閉 channel

  4. 不要依賴任何應用層實現的 channel 關閉檢測方法函數,應該將 channel 的讀寫操作進行分離 (通過不同的 goroutine),並實現只在一個寫入端關閉 channel

  5. 使用 context.Context 控制 channel 的生命週期

  6. 充分考慮緩衝和非緩衝 channel 的使用場景

channel 和鎖如何選擇?

當你發現使用鎖使程序變得複雜時,可以試試使用 channel 會不會使程序變得簡單。

鎖的使用場景

channel 的使用場景

官方給出的建議是除了特殊的、底層的應用程序外,其他情況最好使用 channel 或其他同步原語來完成 (但是從大多數開源組件實現代碼來看,並沒有遵守官方的建議)。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pYbPm6L-chHwmJ_wOf2EIw