[譯] part22: golang channels

咔嘰咔嘰發表於2019-04-02

在上一個教程中,我們討論了 Go 中如何使用Goroutines實現併發。在本教程中,我們將討論有關channel以及Goroutines如何使用channel進行通訊。

什麼是channel

channel可以被認為是Goroutines通訊的管道。類似於水在管道中從一端流到另一端的方式,資料可以從一端傳送,可以從另一端接收。

channel的宣告

每個channel都有一個與之關聯的型別。此型別是允許channel傳輸的資料型別。不允許使用該channel傳輸其他型別。

chan T 代表型別為Tchannel

channel的零值為nilnil channel沒有任何用處,因此得使用類似於make mapmake slice來定義它。

讓我們寫一些宣告channel的程式碼。

package main

import "fmt"

func main() {
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}
複製程式碼

Run in playground

在第 6 行宣告瞭var a chan int,可以看到channel的零值為nil。因此,執行if條件內的語句並定義channel。上面的程式中的a是一個int channel。該程式將輸出,

channel a is nil, going to define it
Type of a is chan int
複製程式碼

使用make宣告也是定義channel的有效而簡潔的方法。

a := make(chan int)
複製程式碼

上面的程式碼行定義了一個int型的channel a

channel的傳送和接收

下面給出了從channel傳送和接收資料的語法,

data := <- a // read from channel a
a <- data // write to channel a
複製程式碼

箭頭相對於通道的方向指定了是傳送還是接收資料。

在第 1 行中,箭頭從a向指向data,因此我們從通道a讀取並將值儲存到變數data中。

在第 2 行中,箭頭指向a,因此我們把data寫入通道a

傳送和接收預設是阻塞的

預設情況下,傳送和接收是阻塞的。這是什麼意思?當資料傳送到channel時,傳送方被阻塞直到其他Goroutine從該channel讀取出資料。類似地,當從channel讀取資料時,讀取方被阻塞,直到其他Goroutine將資料寫入該channel

channel的這種屬性有助於Goroutines有效地進行通訊,而無需使用在其他程式語言中常見的顯式鎖或條件變數。

channel示例程式碼

讓我們編寫一個程式來了解Goroutines如何使用channel進行通訊。

我們在上一篇教程中引用過這個程式。

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}
複製程式碼

Run in playgroud 這是上一個教程的程式碼,這裡我們將使用channel重寫上述程式。

package main

import (
    "fmt"
)

func hello(done chan bool) {
    fmt.Println("Hello world goroutine")
    done <- true
}
func main() {
    done := make(chan bool)
    go hello(done)
    <-done
    fmt.Println("main function")
}
複製程式碼

Run in playgroud

在上面的程式中,我們在第一行建立了一個bool型的done channel。 並將其作為引數傳遞給hello。第 14 行我們正在從done channel接收資料。這行程式碼是阻塞的,這意味著在Goroutine將資料寫入done channel之前將會一直阻塞。因此,上一個程式中的time.Sleep的就沒有必要了,用sleep對程式而言是相當不友好。

程式碼行<-done表示從done channel接收資料,如果沒有任何變數使用或儲存該資料,這是完全合法的。

現在我們的main Goroutine被阻塞直到done channel有資料寫入。 hello Goroutine接收done channel作為引數,列印Hello world goroutine然後把true寫入done channel。當這個寫入完成時,main Goroutine從該done channel接收資料,然後結束阻塞列印了main函式的文字。

輸出,

Hello world goroutine
main function
複製程式碼

讓我們通過在hello Goroutine中引入一個sleep來修改這個程式,以更好地理解這個阻塞概念。

package main

import (
    "fmt"
    "time"
)

func hello(done chan bool) {
    fmt.Println("hello go routine is going to sleep")
    time.Sleep(4 * time.Second)
    fmt.Println("hello go routine awake and going to write to done")
    done <- true
}
func main() {
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <-done
    fmt.Println("Main received data")
}
複製程式碼

Run in playgroud

這個程式將首先列印Main going to call hello go goroutine。然後hello Goroutine啟動,列印hello go routine is going to sleep。列印完成後,hello Goroutine將休眠 4 秒鐘,在此期間main Goroutine將被阻塞,因為它正在等待來自<-done的通道的資料。 4 秒後hello Goroutine甦醒,然後列印hello go routine awake and going to write to done並寫入資料到channel,接著main Goroutine接收資料並列印Main received data

channel 的另外一個例子

讓我們再寫一個程式來更好地理解,該程式將列印數字各個位的平方和立方的總和。

例如,如果 123 是輸入,則此程式將計算輸出為

squares = (1 * 1) + (2 * 2) + (3 * 3) cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) output = squares + cubes = 50

我們將構建該程式,使得平方在一個Goroutine中計算,而立方在另一個Goroutine中進行計算,最終在main Goroutine中求和。

package main

import (
    "fmt"
)

func calcSquares(number int, squareop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares + cubes)
}
複製程式碼

Run in playgroud

calcSquares函式計算各個數字的平方的和,並將其傳送到squares channel。類似地,calcCubes計算各個數字的立方的和並將其傳送到cubes channel

這兩個函式都作為單獨的Goroutines執行。每個函式都通過一個channel作為入參。main Goroutine等待來自這兩個channel的資料。一旦從兩個channel接收到資料,它們就儲存在squarescubes中求和,然後列印最終輸出。該程式將列印,

Final output 1536
複製程式碼

死鎖

使用channel時要考慮的一個重要因素是死鎖。如果Goroutine正在channel上傳送資料,那麼期待其他一些Goroutine接收資料。如果傳送的資料沒有被消費,程式將在執行時產生一個panic

同樣,如果Goroutine正在等待從一個channel接收資料,那麼其他Goroutine應該在該channel上寫入資料,否則程式也會出現panic

package main


func main() {
    ch := make(chan int)
    ch <- 5
}
複製程式碼

Run in playgroud

在上面的程式中,建立了一個channel ch,我們用ch <-5channel傳送 5。在該程式中,沒有其他Goroutinech接收資料。因此,此程式將出現以下執行時錯誤。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox249677995/main.go:6 +0x80
複製程式碼

單向channel

到目前為止我們討論的所有channel都是雙向channel,即資料可以在它們上傳送和接收。也可以建立單向channel,即僅傳送或接收資料的channel

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}
複製程式碼

Run in playgroud

在上面的程式中,我們在第 10 行中建立了僅傳送channel sendchchan < - int表示當箭頭指向chan時僅為傳送channel。我們在第 12 行中嘗試從該channel接收資料。 發現這是不允許的,當程式編譯時,編譯器會報錯,

main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)
複製程式碼

看起來好像沒啥問題,但是一個寫channel僅僅用來寫,而不能用來讀這樣有啥意義!

我們接下來將用到channel轉化。可以將雙向channel轉換為僅傳送或僅接收的channel,反之亦然。

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}
複製程式碼

Run in playgroud

在上面的程式第 10 行,建立了雙向channel chnl。在第 11 行,它作為引數傳遞給sendData Goroutine,而sendData函式在第 5 行用sendch chan < - int將此chnl轉換為僅傳送的channel型別。所以現在通道只在sendData Goroutine中是單向的,但它在main Goroutine中是雙向的。該程式將列印 10 作為輸出。(譯者注:這就是單向channel的用途,定義函式或者方法的時候,使用只讀或只寫會讓程式碼更健壯。)

關閉channel和迴圈channel

傳送者能夠關閉channel以通知接收者不再在該channel上傳送資料。

接收者可以在從channel接收資料時使用額外的變數來檢查channel是否已關閉。

v, ok := <- ch
複製程式碼

在上面的語句中,如果成功地從該操作中接收到該值,則oktrue。如果okfalse,則表示我們正在從一個關閉的channel中讀取。從關閉的channel中讀取的值將是通道型別的零值。例如,如果是int型別,則從關閉的channel中讀取到的值將為 0。

package main

import (
    "fmt"
)

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {
    ch := make(chan int)
    go producer(ch)
    for {
        v, ok := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received ", v, ok)
    }
}
複製程式碼

Run in playgroud

在上面的程式中,生產者Goroutine將 0 到 9 寫入channel chnl,然後關閉它。在第 16 行main函式有一個無限for迴圈,它使變數ok檢查channel是否被關閉。如果okfalse,則表示已關閉,因此迴圈中斷。否則,將列印收到的值和ok的值。這個程式將列印,

Received  0 true
Received  1 true
Received  2 true
Received  3 true
Received  4 true
Received  5 true
Received  6 true
Received  7 true
Received  8 true
Received  9 true
複製程式碼

for 迴圈的for range形式可用於從channel接收值,直到它被關閉。

讓我們使用for range迴圈重寫上面的程式。

package main

import (
    "fmt"
)

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received ",v)
    }
}
複製程式碼

Run in playgroud

for range迴圈在第 16 行接收來自channel ch的資料直到它被關閉。 ch關閉後,迴圈自動退出。該程式輸出,

Received  0
Received  1
Received  2
Received  3
Received  4
Received  5
Received  6
Received  7
Received  8
Received  9
複製程式碼

我們來重寫一下上面那個求平方立方和的程式,

如果仔細檢視程式,可以注意到在calcSquares函式和calcCubes函式中獲取每一位的數字的邏輯重複了。我們將該邏輯的程式碼抽出來,然後分別在那兩個函式中併發呼叫這個函式。

package main

import (
    "fmt"
)

func digits(number int, dchnl chan int) {
    for number != 0 {
        digit := number % 10
        dchnl <- digit
        number /= 10
    }
    close(dchnl)
}
func calcSquares(number int, squareop chan int) {
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit * digit
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares+cubes)
}
複製程式碼

Run in playgroud

上面程式中的digits函式現在包含從number中獲取各位的邏輯,並且它同時由calcSquarescalcCubes函式呼叫。一旦number中沒有更多的位,channel就會在第 13 行被關閉。 calcSquarescalcCubes Goroutines使用for range迴圈監聽各自的channel,直到它關閉。該程式的其餘部分和之前的例子是相同的。該程式也會列印

Final output 1536
複製程式碼

該節教程就結束了,channel中還有更多的概念,例如緩衝channelworker poolselect。我們將在下一個教程中討論它們。

相關文章