Go channel FAQ
close channel 設計理念
func closechan(c *hchan) {
// 關閉一個 nil channel, 拋出 panic
if c == nil {
panic(plainError("close of nil channel"))
}
}
爲什麼關閉一個已經關閉的 channel
會 panic
?
官方這樣設計的初衷,應該是希望開發者不要依賴於 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 最佳實踐
-
channel 類型作爲參數時,指定操作類型 (讀 / 寫)
-
使用
select + default
處理多個channel
輪詢場景 -
永遠不要在讀取方向關閉
channel
, 只在寫入端關閉 channel -
不要依賴任何應用層實現的
channel
關閉檢測方法函數,應該將channel
的讀寫操作進行分離 (通過不同的goroutine
),並實現只在一個寫入端關閉 channel -
使用
context.Context
控制channel
的生命週期 -
充分考慮緩衝和非緩衝
channel
的使用場景
-
無緩衝
channel
提供了阻塞機制,雖然避免了數據競態,但是當數據較多時降低了性能,而且可能引發死鎖 -
緩衝
channel
雖然避免了阻塞,但是有潛在的數據競態,而且需要考慮緩衝區大小,設計不合理容易浪費資源
channel 和鎖如何選擇?
當你發現使用鎖使程序變得複雜時,可以試試使用 channel 會不會使程序變得簡單。
鎖的使用場景
-
訪問共享數據結構中的緩存信息
-
保存應用程序上下文和狀態信息
-
保護某個結構內部狀態和完整性
-
高性能要求的臨界區代碼
channel 的使用場景
-
線程 (goroutine) 通信
-
併發通信
-
異步操作
-
任務分發
-
傳遞數據所有權
-
數據邏輯組合 (如 Pipeline, FanIn FanOut 等併發模式)
官方給出的建議是除了特殊的、底層的應用程序外,其他情況最好使用 channel
或其他同步原語來完成 (但是從大多數開源組件實現代碼來看,並沒有遵守官方的建議)。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pYbPm6L-chHwmJ_wOf2EIw