Golang 教學系列 - close 與 for range 搭配 channel 觀念教學

上次的文章介紹:單雙向 channel 的差別及宣告方式,可以參考:Golang 教學系列 - channel 裡面資料什麼都能放?單雙向 channel 的差別

今天這篇文章主要介紹如何對 channel 進行關閉以及如何透過 for range 來尋訪 channel 裡面的 element。如果想要詳細的介紹可以參考我的教學影片:Golang 教學系列 - close 與 for range 搭配 channel 觀念教學!| 肯尼攻城獅

關閉 channel

直接上程式碼:

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

import "fmt"

func main() {
intChan := make(chan int, 3)
intChan <- 1
intChan <- 2
close(intChan)
intChan <- 3
}

上面我宣告了一個 channel 變數,裡面可以放的型態是 int。然後透過一個 keyword:close 可以將 channel 關閉。

  • 一旦 channel 關閉之後就不能再往 channel 裡面送資料了。

  • 如果持續送資料會出現 panic 的錯誤,所以如果跑上面的程式就會出現以下的錯誤:

    1
    panic: send on closed channel
  • 雖然 channel 被關閉但仍是可以從裡面取出資料。

    例如:

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

    import "fmt"

    func main() {
    intChan := make(chan int, 3)
    intChan <- 1
    intChan <- 2
    close(intChan)
    a := <- intChan
    fmt.Println(a)
    }

    這樣依舊可以從 channel 裡面取出值並將值賦予給 a。

for loop 從 channel 裡面取資料

直接上程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func main() {
num := 100
intChan := make(chan int, num)

for i := 0; i < num; i++ {
intChan <- i
}

for i := 0; i < num; i++ {
fmt.Println(<- intChan)
}
}

這個範例是只有主程式來對 channel for loop 取出資料,最直覺的方式當然就是 channel 裡面有多少元素我就取幾次的作法。所以在第一個 for loop 我將元素放入 channel 放到滿,再透過第二個 for loop 將全部元素取出來

缺點在於:這樣之所以沒問題是因為我是把 channel 容量塞到滿,但很多情況 channel 不會被塞滿,然後我就想取出來了,可是我怎麼知道我 for loop 要取幾次?

邊送邊從 channel 取資料

有的情境是 channel 再被送資料的同時,我有另外一個 goroutine 可以就取出來,或是另外一種情境是這個 channel 送完資料就會關閉了,我另外一邊的 goroutine 要把資料取完。

透過 for range 與 close 的搭配就可以。

請看程式碼:

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

import "fmt"

func main() {
num := 100
intChan := make(chan int, 100)
for i := 0; i < num; i++ {
intChan <- i
}
//close(intChan)

for i := range intChan {
fmt.Println(i)
}
}

這邊先不用 close,單純使用 for range,透過 for range 可以簡單的尋訪 channel 裡面的元素,會將 channel 裡面元素的值賦予給 i。

但是運行該程式還是會出現 deadlock 錯誤:

1
fetal error: all goroutines are asleep - deadlock!

因為這個 for range 會一直不斷從 channel 裡面取出資料,如果 channel 被取到沒有資料就會被 block 住,直到 channel 裡面有資料為止,因此造成 deadlock。

這就是為什麼如果想要避免這樣的錯誤就可以使用 close channel 的動作。透過 close channel,for range 取出所有資料後就會自動往下執行,而不會被 block 住。

再看一個 for range 例子

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
30
31
package main

import (
"fmt"
"math/rand"
"time"
)

func generateNumbers(intChan chan int) {
count := 0
for {
if count != 6 {
num := rand.Intn(10) + 1
intChan <- num
fmt.Println("put num:", num)
count++
} else {
close(intChan)
break
}
}
}

func main() {
intChan := make(chan int, 1)
go generateNumbers(intChan)
for num := range intChan {
fmt.Println("get num:", num)
time.Sleep(time.Second)
}
}

主程式主要是:

  • 宣告 channel 為容量為 1
  • 開啟一個 goroutine 來產生數字丟進 channel
  • 使用 for range 對 channel 裡面取出資料並且印出

generateNumbers 主要作用是:

用一個 for loop 去隨機產生 1~10 的數字,然後將數字丟進 channel 裡面,而這個 for loop 只會運行 6 次。當判斷 count 是 6 的時候則將 channel 關閉並 break for loop。

這樣的寫法可以知道由於一個 goroutine 再放資料,另一個 goroutine 再取資料,而當 for range 這邊取資料如果比較快的話這邊就會持續 block 住,直到 channel 裡面還有資料可以取為止,或是如果 channel 被關閉的話就代表資料已經取完可以離開這個 for range。

for range 搭配 bool,value 取值的方式

來看程式碼:

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
30
31
32
33
34
35
package main

import (
"fmt"
"math/rand"
"time"
)

func generateNumbers(intChan chan int) {
count := 0
for {
if count != 6 {
num := rand.Intn(10) + 1
intChan <- num
fmt.Println("put num:", num)
count++
} else {
close(intChan)
break
}
}
}

func main() {
intChan := make(chan int, 1)
go generateNumbers(intChan)
for {
if num, ok := <- intChan; ok {
fmt.Printf("isSuccess:%v value: %d\n", ok, num)
time.Sleep(time.Second)
} else {
break
}
}
}

這邊可以看到主程式這邊從 channel 取出資料的時候可以有兩個回傳值,第二個值是 bool 型態是代表說 channel 裡面取不取的到資料。這個取不取得到資料看的是 channel 有沒有被關閉,只有當 channel 被關閉的時候這邊的 bool 值才會回傳 false,所以如果單純取資料的話如果取不到的話是會一直被 block 住的。

總結

今天主要介紹如何透過 for range 與 close 來尋訪 channel 裡面的資料。也是介紹一種方式來讓 main goroutine 等待其他 goroutine 的方式。下一篇文章會介紹 channel 常見的應用範例。