10.02-Channel

Channel

Channel (頻道) 提供一種方式,供兩個 goroutines 可以互相通信,並彼此同步執行。下列是一個使用 channel 的範例程式:

package main

import (
    "fmt"
    "time"
)

func pinger(c chan string) {
    for i := 0; ; i++ {
        c <- "ping"
    }
}
func printer(c chan string) {
    for {
        msg := <- c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}
func main() {
    var c chan string = make(chan string)
    
    go pinger(c)
    go printer(c)
    
    var input string
    fmt.Scanln(&input)
}

此程式將會持續印出 "ping"(直到按下 Enter 為止)。一個 channel type (頻道型別) 的表示方式是使用關鍵字 chan 以及接著要傳遞給 channel 的資料之型別(在此例中,我們傳遞字串)。

而 <- (左箭頭)運算符則用於傳送與接收 channel 的訊息,c <- "ping" 表示送出 "ping",而 msg := <- c 表示接收一個訊息,並將訊息儲存於 msgfmt 這行也可以寫成:fmt.Println(<-c),在此例我們可以移除前面那行。

像這樣使用一個 channel 來同步兩個 two goroutines。當 pinger 試圖想要送出該 channel 上的一筆訊息時,它會持續等待(即所謂的阻塞),直到 printer 已經準備好要接收訊息為止。我們將另一個傳送端加到程式中,看會發生什麼事情。新增如下的函式:

func ponger(c chan string) {
    for i := 0; ; i++ {
        c <- "pong"
    }
}

再修改 main

func main() {
    var c chan string = make(chan string)
    
    go pinger(c)
    go ponger(c)
    go printer(c)
    
    var input string
    fmt.Scanln(&input)
}

這個程式現在會輪流印出 “ping” 跟 “pong”。

Channel Direction

我們可以對一個 channel 型別指定 channel direction 來限制它是要做為傳送或是接收。例如:會將 pinger 的函式特徵值修改如下:

func pinger(c chan<- string)

現在 c 只能用來傳送,若企圖對 c 進行接收資料將會導致一個編譯的錯誤,我們一樣可以將 printer 改為如下:

func printer(c <-chan string)

若 channel 沒有方向限制,則是所謂的雙向(bi-directional),可以將一個雙向的 channel 傳遞給一個函式做為只能傳送或只能接收的 channel 使用,反之則不可。

Select

Go 有一個特殊的陳述句,稱為 select,它的作用與 switch 類似,但只能用在 channels:

func main() {
    c1 := make(chan string)
    c2 := make(chan string)
    
    go func() {
        for {
            c1 <- "from 1"
            time.Sleep(time.Second * 2)
        }
    }()
    go func() {
        for {
            c2 <- "from 2"
            time.Sleep(time.Second * 3)
        }
    }()
    go func() {
        for {
            select {
            case msg1 := <- c1:
                fmt.Println(msg1)
            case msg2 := <- c2:
                fmt.Println(msg2)
            }
        }
    }()
    
    var input string
    fmt.Scanln(&input)
}

這個程式每隔兩秒會輸出一次 “from 1”,而每隔三秒會印出一次 “from 2”。select 會選取第一個已經就緒的 channel 來接收(或傳送)資料。若同時有多個 channels 已經就緒,則隨機選取一個 channel 來進行接收。若沒有任何 channel 已經就緒,則會發生阻塞,直到有任何一個 channel 已經就緒為止。

select 陳述句也很常用來實做計時工作(timeout):

select {
case msg1 := <- c1:
    fmt.Println("Message 1", msg1)
case msg2 := <- c2:
    fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
    fmt.Println("timeout")
}

time.After 會建立一個 channel,並在指定的時間週期之後送出它所在的目前時間(我們用不到這個時間,所以這裡不會把時間存到變數)。我們也能指定一個 default 的情況:

select {
case msg1 := <- c1:
    fmt.Println("Message 1", msg1)
case msg2 := <- c2:
    fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
    fmt.Println("timeout")
default:
    fmt.Println("nothing ready")
}

若沒有任何 channel 已經就緒,則會進去 default 這個情況。

Buffered Channel

在建立一個 channel 時,也可以傳遞第二個參數給 make 函式:

c := make(chan int, 1)

這樣會建立一個容量為1的 buffered channel(有緩衝的頻道),通常 channel 是同步的,channel 兩端會互相等待彼此已經就緒。不過一個有緩衝的 channel 則是非同步的(asynchronous),在傳送或接收一筆訊息時都不用等待,除非 channel 已經滿了。

Comments