Golang 教學系列 - 何謂 Channel? 先從宣告 Channel 開始學起!

上次講了 WaitGroup 如何應用在 Goroutine 上,以及常見的坑:Golang 教學系列 - WaitGroup 常見的坑以及應用介紹

今天這篇文章要介紹的是何謂 Channel?先從宣告 Channel 開始學起!

如果想要知道更詳細的講解可以參考我的影片:Golang 教學系列 - 何謂 Channel? 先從宣告 Channel 開始學起!| 肯尼攻城獅

Channel 是什麼

  • Channel 是一種型態一種資料型態
  • Channel 是管道的意思,在裡面可以放一些資料,而裡面的資料可以透過 Goroutine 放進去

Channel 宣告方式

宣告 channel

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
var intChan chan int
fmt.Println(intChan)
}
  • 宣告的時候需要加上 chan keyword 再加上 管道裡面要放怎樣型態的 element,所以上面的例子來看:var intChan chan int 代表了宣告了一個叫做 intChan 的變數,這個變數是 chan 型態,裡面可以放 int 型態的 element
  • 如果只有單純宣告就輸出的話,會輸出成 nil

放入元素到 channel

錯誤用法

如果宣告完 channel 就直接往裡面放元素會導致以下錯誤:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var intChan chan int
fmt.Println(intChan)
intChan <- 10
}
1
fetal error: all goroutines are asleep - deadlock!

之所以會出現 deadlock 錯誤是因為如果只有單純宣告 channel,並沒有初始化要給該 channel 多少容量的話,元素會放不進去,放不進去的話這邊就會一直被 block 住,也就是這個地方被 block 住:intChan <- 10

所以 nil channel 無法往裡面放 element。

正確用法

應該是要宣告的時候同時也要進行初始化容量的動作:

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var intChan chan int
intChan = make(chan int, 1)
intChan <- 10
fmt.Println(<- intChan)
}
  • 透過 make 給予管道內的容量為 1,代表可以放一個元素
  • 透過 <- 來放進元素,透過 <- 放在 channel 變數就代表取出管道內的元素
  • 因此這邊就會輸出為 10

可以透過 len()跟cap() 來做驗證,是否真的從管道內取出元素

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
var intChan chan int
intChan = make(chan int, 1)
fmt.Println(len(intChan))
intChan <- 10
fmt.Println(len(intChan))
fmt.Println(<- intChan)
fmt.Println(len(intChan))
}

這邊 len () 取得的是管道內的元素的數量,所以一開始會是 0,當放進元素後會改成 1,再取出來,之後又會變成 0。

運行之後輸出如下:

1
2
3
4
0
1
10
0

該注意的點

  • channel 內的容量一旦滿了之後,再往裡面放元素會造成 block 的動作,除非有取出的動作才不會被 block 住,如下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package main

    func main() {
    var intChen = make(chan int, 3)
    intChen <- 1
    intChen <- 2
    intChen <- 3
    intChen <- 4
    }

    intChan 這邊會被 block 住而無法往裡面丟 4

    所以可以改成取出來:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package main

    func main() {
    var intChen = make(chan int, 3)
    intChen <- 1
    intChen <- 2
    intChen <- 3
    <- intChan
    intChen <- 4
    }

    這樣就可以往裡面放 4,然後取出來的元素可以給一個變數或是也不給,不給的話就像上面那樣單純取出來就可以了

    這邊可以注意的是,取出來的順序是先進先出,所以這邊取出來是取出 1。可以輸出來做驗證:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package main

    func main() {
    var intChen = make(chan int, 3)
    intChen <- 1
    intChen <- 2
    intChen <- 3
    b := <- intChan
    intChen <- 4
    fmt.Println(b)
    }

    會輸出為 1。

0 個容量的 channel

如果這樣宣告:

1
2
3
4
5
6
package main

func main() {
stringChan := make(chan string, 0)
stringChan2 := make(chan string)
}
  • stringChan 跟 stringChan2 都是 0 個容量的意思,如果第二個參數省略不寫預設就是 0。

  • 0 個容量代表既不能往 channel 丟資料也不能從 channel 裡面取資料

  • 假設有兩個 Goroutine 要透過 0 個容量 channel 溝通的話,可以這樣寫,就不會造成 deadlock:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package main

    import "fmt"

    func main() {
    stringChan := make(chan string)
    go func() {
    stringChan <- "hello world"
    }()
    fmt.Println(<- stringChan)
    }

    這樣不會被 block 住是因為,同時有兩個 Goroutine 往裡面取資料跟放資料這樣不會造成 deadlock。

    而且這樣也可以拿到資料。

總結

這篇文章介紹了 channel 的宣告方式及放入跟取出元素的方式,同時也介紹會發生 deadlock 的情況以及如何避免。下篇文章會介紹 channel 裡面還能放怎樣的資料,以及單雙向的 channel 的差別?以這篇文章的例子都是雙向的 channel 也就是可以往裡面丟資料以及取資料。

最後最後!請聽我一言!

如果你還沒有註冊 Like Coin,你可以透過我的邀請註冊連結來免費註冊,註冊完後就可以在文章最下方幫我按下 Like 按鈕,而 Like 最多可以點五次,如此一來你不用付出任何一塊錢,就能給我寫這篇文章最大的回饋!