總結了才知道,原來channel有這么多用法!


這篇文章總結了channel的11種常用操作,以一個更高的視角看待channel,會給大家帶來對channel更全面的認識。

在介紹11種操作前,先簡要介紹下channel的使用場景、基本操作和注意事項。

channel的使用場景

把channel用在數據流動的地方:

  1. 消息傳遞、消息過濾
  2. 信號廣播
  3. 事件訂閱與廣播
  4. 請求、響應轉發
  5. 任務分發
  6. 結果匯總
  7. 並發控制
  8. 同步與異步

channel的基本操作和注意事項

channel存在3種狀態

  1. nil,未初始化的狀態,只進行了聲明,或者手動賦值為nil
  2. active,正常的channel,可讀或者可寫
  3. closed,已關閉,千萬不要誤認為關閉channel后,channel的值是nil

channel可進行3種操作

  1. 關閉

把這3種操作和3種channel狀態可以組合出9種情況

操作 nil的channel 正常channel 已關閉channel
<- ch 阻塞 成功或阻塞 讀到零值
ch <- 阻塞 成功或阻塞 panic
close(ch) panic 成功 panic

對於nil通道的情況,也並非完全遵循上表,有1個特殊場景:當nil的通道在select的某個case中時,這個case會阻塞,但不會造成死鎖。

參考代碼請看:https://dave.cheney.net/2014/03/19/channel-axioms

下面介紹使用channel的10種常用操作。

1. 使用for range讀channel

場景

當需要不斷從channel讀取數據時。

原理

使用for-range讀取channel,這樣既安全又便利,當channel關閉時,for循環會自動退出,無需主動監測channel是否關閉,可以防止讀取已經關閉的channel,造成讀到數據為通道所存儲的數據類型的零值。

用法

1
2
3
for x := range ch{
fmt.Println(x)
}

2. 使用v,ok := <-ch + select操作判斷channel是否關閉

場景

v,ok := <-ch + select操作判斷channel是否關閉

原理

ok的結果和含義:

- `true`:讀到通道數據,不確定是否關閉,可能channel還有保存的數據,但channel已關閉。
- `false`:通道關閉,無數據讀到。

從關閉的channel讀值讀到是channel所傳遞數據類型的零值,這個零值有可能是發送者發送的,也可能是channel關閉了。

_, ok := <-ch與select配合使用的,當ok為false時,代表了channel已經close。下面解釋原因,_,ok := <-ch對應的函數是func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool),入參block含義是當前goroutine是否可阻塞,當block為false代表的是select操作,不可阻塞當前goroutine的在channel操作,否則是普通操作(即_, ok不在select中)。返回值selected代表當前操作是否成功,主要為select服務,返回received代表是否從channel讀到有效值。它有3種返回值情況:

  1. block為false,即執行select時,如果channel為空,返回(false,false),代表select操作失敗,沒接收到值。
  2. 否則,如果channel已經關閉,並且沒有數據,ep即接收數據的變量設置為零值,返回(true,false),代表select操作成功,但channel已關閉,沒讀到有效值。
  3. 否則,其他讀到有效數據的情況,返回(true,ture)。

我們考慮_, ok := <-chselect結合使用的情況。

情況1:當chanrecv返回(false,false)時,本質是select操作失敗了,所以相關的case會阻塞,不會執行,比如下面的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
ch := make(chan int)
select {
case v, ok := <-ch:
fmt.Printf("v: %v, ok: %v\n", v, ok)
default:
fmt.Println("nothing")
}
}

// 結果:
// nothing

情況2:下面的結果會是零值和false:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
ch := make(chan int)

// 增加關閉
close(ch)

select {
case v, ok := <-ch:
fmt.Printf("v: %v, ok: %v\n", v, ok)
}
}

// v: 0, ok: false

情況3的received為true,即_, ok中的ok為true,不做討論了,只討論ok為false的情況。

最后ok為false的時候,只有情況2,此時channel必然已經關閉,我們便可以在select中用ok判斷channel是否已經關閉。

用法

下面例子展示了,向channel寫數據然后關閉,依然可以從已關閉channel讀到有效數據,但channel關閉且沒有數據時,讀不到有效數據,ok為false,可以確定當前channel已關閉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// demo_select6.go
func main() {
ch := make(chan int, 1)

// 發送1個數據關閉channel
ch <- 1
close(ch)
print("close channel\n")

// 不停讀數據直到channel沒有有效數據
for {
select {
case v, ok := <-ch:
print("v: ", v, ", ok:", ok, "\n")
if !ok {
print("channel is close\n")
return
}
default:
print("nothing\n")
}
}
}

// 結果
// close channel
// v: 1, ok:true
// v: 0, ok:false
// channel is close

更多見golang_step_by_step/channel/ok倉庫中ok和select的示例,或者閱讀channel源碼。

3. 使用select處理多個channel

場景

需要對多個通道進行同時處理,但只處理最先發生的channel時

原理

select可以同時監控多個通道的情況,只處理未阻塞的case。當通道為nil時,對應的case永遠為阻塞,無論讀寫。特殊關注:普通情況下,對nil的通道寫操作是要panic的。

用法

1
2
3
4
5
6
7
8
9
// 分配job時,如果收到關閉的通知則退出,不分配job
func (h *Handler) handle(job *Job) {
select {
case h.jobCh<-job:
return
case <-h.stopCh:
return
}
}

4. 使用channel的聲明控制讀寫權限

場景

協程對某個通道只讀或只寫時

目的:

  1. 使代碼更易讀、更易維護,
  2. 防止只讀協程對通道進行寫數據,但通道已關閉,造成panic。

用法

  • 如果協程對某個channel只有寫操作,則這個channel聲明為只寫。
  • 如果協程對某個channel只有讀操作,則這個channe聲明為只讀。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 只有generator進行對outCh進行寫操作,返回聲明
// <-chan int,可以防止其他協程亂用此通道,造成隱藏bug
func generator(int n) <-chan int {
outCh := make(chan int)
go func(){
for i:=0;i<n;i++{
outCh<-i
}
}()
return outCh
}

// consumer只讀inCh的數據,聲明為<-chan int
// 可以防止它向inCh寫數據
func consumer(inCh <-chan int) {
for x := range inCh {
fmt.Println(x)
}
}

5. 使用緩沖channel增強並發

場景

異步

原理

有緩沖通道可供多個協程同時處理,在一定程度可提高並發性。

用法

1
2
3
4
5
// 無緩沖
ch1 := make(chan int)
ch2 := make(chan int, 0)
// 有緩沖
ch3 := make(chan int, 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用5個`do`協程同時處理輸入數據
func test() {
inCh := generator(100)
outCh := make(chan int, 10)

for i := 0; i < 5; i++ {
go do(inCh, outCh)
}

for r := range outCh {
fmt.Println(r)
}
}

func do(inCh <-chan int, outCh chan<- int) {
for v := range inCh {
outCh <- v * v
}
}

6. 為操作加上超時

場景

需要超時控制的操作

原理

使用selecttime.After,看操作和定時器哪個先返回,處理先完成的,就達到了超時控制的效果

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func doWithTimeOut(timeout time.Duration) (int, error) {
select {
case ret := <-do():
return ret, nil
case <-time.After(timeout):
return 0, errors.New("timeout")
}
}

func do() <-chan int {
outCh := make(chan int)
go func() {
// do work
}()
return outCh
}

7. 使用time實現channel無阻塞讀寫

場景

並不希望在channel的讀寫上浪費時間

原理

是為操作加上超時的擴展,這里的操作是channel的讀或寫

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func unBlockRead(ch chan int) (x int, err error) {
select {
case x = <-ch:
return x, nil
case <-time.After(time.Microsecond):
return 0, errors.New("read time out")
}
}

func unBlockWrite(ch chan int, x int) (err error) {
select {
case ch <- x:
return nil
case <-time.After(time.Microsecond):
return errors.New("read time out")
}
}

注:time.After等待可以替換為default,則是channel阻塞時,立即返回的效果

8. 使用close(ch)關閉所有下游協程

場景

退出時,顯示通知所有協程退出

原理

所有讀ch的協程都會收到close(ch)的信號

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (h *Handler) Stop() {
close(h.stopCh)

// 可以使用WaitGroup等待所有協程退出
}

// 收到停止后,不再處理請求
func (h *Handler) loop() error {
for {
select {
case req := <-h.reqCh:
go handle(req)
case <-h.stopCh:
return
}
}
}

9. 使用chan struct{}作為信號channel

場景

使用channel傳遞信號,而不是傳遞數據時

原理

沒數據需要傳遞時,傳遞空struct

用法

1
2
3
4
5
6
// 上例中的Handler.stopCh就是一個例子,stopCh並不需要傳遞任何數據
// 只是要給所有協程發送退出的信號
type Handler struct {
stopCh chan struct{}
reqCh chan *Request
}

10. 使用channel傳遞結構體的指針而非結構體

場景

使用channel傳遞結構體數據時

原理

channel本質上傳遞的是數據的拷貝,拷貝的數據越小傳輸效率越高,傳遞結構體指針,比傳遞結構體更高效

用法

1
2
3
4
reqCh chan *Request

// 好過
reqCh chan Request

11. 使用channel傳遞channel

場景

使用場景有點多,通常是用來獲取結果。

原理

channel可以用來傳遞變量,channel自身也是變量,可以傳遞自己。

用法

下面示例展示了有序展示請求的結果,另一個示例可以見另外文章的版本3


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM