Go Channel 詳解

webck 8年前發布 | 10K 次閱讀 Go語言 Google Go/Golang開發

Channel是Go中的一個核心類型,你可以把它看成一個管道,通過它并發核心單元就可以發送或者接收數據進行通訊(communication)。

它的操作符是箭頭 <-

ch <- v // 發送值v到Channel ch中
v := <-ch // 從Channel ch中接收數據,并將數據賦值給v

(箭頭的指向就是數據的流向)

就像 map 和 slice 數據類型一樣, channel必須先創建再使用:

ch := make(chanint)

Channel類型

Channel類型的定義格式如下:

ChannelType= ("chan"|"chan""<-"|"<-""chan")ElementType.

它包括三種類型的定義。可選的 <- 代表channel的方向。如果沒有指定方向,那么Channel就是雙向的,既可以接收數據,也可以發送數據。

chanT// 可以接收和發送類型為 T 的數據
chan<-float64// 只可以用來發送 float64 類型的數據
<-chanint// 只可以用來接收 int 類型的數據

<- 總是優先和最左邊的類型結合。(The <- operator associates with the leftmost chan possible)

chan<-chanint// 等價 chan<- (chan int)
chan<- <-chanint// 等價 chan<- (<-chan int)
<-chan<-chanint// 等價 <-chan (<-chan int)
chan(<-chanint)

使用 make 初始化Channel,并且可以設置容量:

make(chanint,100)

容量(capacity)代表Channel容納的最多的元素的數量,代表Channel的緩存的大小。如果沒有設置容量,或者容量設置為0, 說明Channel沒有緩存,只有sender和receiver都準備好了后它們的通訊(communication)才會發生(Blocking)。如果設置了緩存,就有可能不發生阻塞, 只有buffer滿了后 send才會阻塞, 而只有緩存空了后receive才會阻塞。一個nil channel不會通信。

可以通過內建的 close 方法可以關閉Channel。

你可以在多個goroutine從/往 一個channel 中 receive/send 數據, 不必考慮額外的同步措施。

Channel可以作為一個先入先出(FIFO)的隊列,接收的數據和發送的數據的順序是一致的。

channel的 receive支持 multi-valued assignment ,如

v, ok := <-ch

它可以用來檢查Channel是否已經被關閉了。

  1. send語句

    send語句用來往Channel中發送數據, 如 ch <- 3 。

    它的定義如下:

SendStmt = Channel "<-"Expression .
Channel = Expression .

在通訊(communication)開始前channel和expression必選先求值出來(evaluated),比如下面的(3+4)先計算出7然后再發送給channel。

c := make(chanint)
deferclose(c)
gofunc() { c <-3+4}()
i := <-c
fmt.Println(i)

send被執行前(proceed)通訊(communication)一直被阻塞著。如前所言,無緩存的channel只有在receiver準備好后send才被執行。如果有緩存,并且緩存未滿,則send會被執行。

往一個已經被close的channel中繼續發送數據會導致 run-time panic

往nil channel中發送數據會一致被阻塞著。

  1. receive 操作符

    <-ch 用來從channel ch中接收數據,這個表達式會一直被block,直到有數據可以接收。

從一個nil channel中接收數據會一直被block。

從一個被close的channel中接收數據不會被阻塞,而是立即返回,接收完已發送的數據后會返回元素類型的零值(zero value)。

如前所述,你可以使用一個額外的返回參數來檢查channel是否關閉。

x, ok := <-ch
x, ok = <-ch
varx, ok = <-ch

如果OK 是false,表明接收的x是產生的零值,這個channel被關閉了或者為空。

blocking

缺省情況下,發送和接收會一直阻塞著,知道另一方準備好。這種方式可以用來在gororutine中進行同步,而不必使用顯示的鎖或者條件變量。

如官方的例子中 x, y := <-c, <-c 這句會一直等待計算結果發送到channel中。

import"fmt"

funcsum(s []int, cchanint) {
 sum :=0
for_, v :=ranges {
 sum += v
 }
 c <- sum // send sum to c
}

funcmain() {
 s := []int{7,2,8,-9,4,0}

 c := make(chanint)
gosum(s[:len(s)/2], c)
gosum(s[len(s)/2:], c)
 x, y := <-c, <-c // receive from c

 fmt.Println(x, y, x+y)
}

Buffered Channels

make的第二個參數指定緩存的大小: ch := make(chan int, 100) 。

通過緩存的使用,可以盡量避免阻塞,提供應用的性能。

Range

for …… range 語句可以處理Channel。

funcmain() {
gofunc() {
 time.Sleep(1* time.Hour)
 }()
 c := make(chanint)
gofunc() {
fori :=0; i <10; i = i +1{
 c <- i
 }
close(c)
 }()

fori :=rangec {
 fmt.Println(i)
 }

 fmt.Println("Finished")
}

range c 產生的迭代值為Channel中發送的值,它會一直迭代知道channel被關閉。上面的例子中如果把 close(c) 注釋掉,程序會一直阻塞在 for …… range 那一行。

select

select 語句選擇一組可能的send操作和receive操作去處理。它類似 switch ,但是只是用來處理通訊(communication)操作。

它的 case 可以是send語句,也可以是receive語句,亦或者 default 。

receive 語句可以將值賦值給一個或者兩個變量。它必須是一個receive操作。

最多允許有一個 default case ,它可以放在case列表的任何位置,盡管我們大部分會將它放在最后。

import"fmt"

funcfibonacci(c, quitchanint) {
 x, y :=0,1
for{
select{
casec <- x:
 x, y = y, x+y
case<-quit:
 fmt.Println("quit")
return
 }
 }
}

funcmain() {
 c := make(chanint)
 quit := make(chanint)
gofunc() {
fori :=0; i <10; i++ {
 fmt.Println(<-c)
 }
 quit <-0
 }()
 fibonacci(c, quit)
}

如果有同時多個case去處理,比如同時有多個channel可以接收數據,那么Go會偽隨機的選擇一個case處理(pseudo-random)。如果沒有case需要處理,則會選擇 default 去處理,如果 default case 存在的情況下。如果沒有 default case ,則 select 語句會阻塞,直到某個case需要處理。

需要注意的是,nil channel上的操作會一直被阻塞,如果沒有default case,只有nil channel的select會一直被阻塞。

select 語句和 switch 語句一樣,它不是循環,它只會選擇一個case來處理,如果想一直處理channel,你可以在外面加一個無限的for循環:

for{
select{
casec <- x:
 x, y = y, x+y
case<-quit:
 fmt.Println("quit")
return
 }
}

timeout

select 有很重要的一個應用就是超時處理。 因為上面我們提到,如果沒有case需要處理,select語句就會一直阻塞著。這時候我們可能就需要一個超時操作,用來處理超時的情況。

下面這個例子我們會在2秒后往channel c1中發送一個數據,但是 select 設置為1秒超時,因此我們會打印出 timeout 1 ,而不是 result 1 。

import"time"
import"fmt"

funcmain() {
 c1 := make(chanstring,1)
gofunc() {
 time.Sleep(time.Second *2)
 c1 <- "result 1"
 }()

select{
caseres := <-c1:
 fmt.Println(res)
case<-time.After(time.Second *1):
 fmt.Println("timeout 1")
 }
}

其實它利用的是 time.After 方法,它返回一個類型為 <-chan Time 的單向的channel,在指定的時間發送一個當前時間給返回的channel中。

Timer和Ticker

我們看一下關于時間的兩個Channel。timer是一個定時器,代表未來的一個單一事件,你可以告訴timer你要等待多長時間,它提供一個Channel,在將來的那個時間那個Channel提供了一個時間值。下面的例子中第二行會阻塞2秒鐘左右的時間,直到時間到了才會繼續執行。

timer1 := time.NewTimer(time.Second *2)
<-timer1.C
fmt.Println("Timer 1 expired")

當然如果你只是想單純的等待的話,可以使用 time.Sleep 來實現。

你還可以使用 timer.Stop 來停止計時器。

timer2 := time.NewTimer(time.Second)
gofunc() {
 <-timer2.C
 fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
ifstop2 {
 fmt.Println("Timer 2 stopped")
}

ticker 是一個定時觸發的計時器,它會以一個間隔(interval)往Channel發送一個事件(當前時間),而Channel的接收者可以以固定的時間間隔從Channel中讀取事件。下面的例子中ticker每500毫秒觸發一次,你可以觀察輸出的時間。

ticker := time.NewTicker(time.Millisecond *500)
gofunc() {
fort :=rangeticker.C {
 fmt.Println("Tick at", t)
 }
}()

類似timer, ticker也可以通過 Stop 方法來停止。一旦它停止,接收者不再會從channel中接收數據了。

close

內建的close方法可以用來關閉channel。

總結一下channel關閉后sender的receiver操作。

如果channel c已經被關閉,繼續往它發送數據會導致 panic: send on closed channel :

import"time"

funcmain() {
 go func(){
 time.Sleep(time.Hour)
 }()
c:= make(chan int,10)
c<-1
c<-2
 close(c)
c<-3
}

但是從這個關閉的channel中不但可以讀取出已發送的數據,還可以不斷的讀取零值:

c := make(chanint,10)
c <-1
c <-2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0

但是如果通過 range 讀取,channel關閉后for循環會跳出:

c := make(chanint,10)
c <-1
c <-2
close(c)
fori :=rangec {
 fmt.Println(i)
}

通過 i, ok := <-c 可以查看Channel的狀態,判斷值是零值還是正常讀取的值。

c := make(chanint,10)
close(c)

i, ok := <-c
fmt.Printf("%d, %t", i, ok)//0, false

同步

channel可以用在goroutine之間的同步。下面的例子中main goroutine通過done channel等待worker完成任務。 worker做完任務后只需往channel發送一個數據就可以通知main goroutine任務完成。

import(
"fmt"
"time"
)

funcworker(donechanbool) {
 time.Sleep(time.Second)

// 通知任務已完成
 done <- true
}

funcmain() {
 done := make(chanbool,1)
goworker(done)

// 等待任務完成
 <-done
}

參考資料

  1. https://gobyexample.com/channels
  2. https://tour.golang.org/concurrency/2
  3. https://golang.org/ref/spec#Select_statements
  4. https://github.com/a8m/go-lang-cheat-sheet
  5. http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/

來自: http://colobu.com/2016/04/14/Golang-Channels/ 

 本文由用戶 webck 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!