
Go 語言強大的 goroutine 特性,讓各位開發者愛不釋手,而多個 goroutine 如何溝通呢?就是透過 Channel 來做到。本篇教大家從 Channel 讀取資料的兩種方式及使用時機,並實際用一個案例快速了解 Channel 實作上會遇到哪些問題?底下用兩個範例讓大家了解如何讀取 Channel 資料出來。
影片教學
其他線上課程請參考如下
讀取 Channel 兩種方式
第一個用的是 for range 方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}
|
第二種是透過 v, ok := <-ch 方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for {
v, ok := <-ch
if !ok {
return
}
fmt.Println(v)
}
}
|
看完上面兩個範例,開發者也很清楚知道這兩種讀取方式,但是會遇到什麼時候開始用第一種,什麼時候該使用第二種?底下來看看一個簡單範例
兩個 goroutine 交互讀取字元
先看看題目,有一個字串 foobar,將字元拆開丟到 Channel 內,用兩個 goroutine 交互讀取字元,底下是最後的輸出結果
1
2
3
4
5
6
| goroutine01: f
goroutine02: o
goroutine01: o
goroutine02: b
goroutine01: a
goroutine02: r
|
先把上面題目複製到 main.go,大家可以看一下底下範例後,看看怎麼寫出兩個 goroutine,可以先在線上練習看看,不要往下看解答
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| package main
import (
"sync"
)
func main() {
str := []byte("foobar")
ch := make(chan byte, len(str))
wg := &sync.WaitGroup{}
wg.Add(2)
for i := 0; i < len(str); i++ {
ch <- str[i]
}
go func() {
}()
go func() {
}()
wg.Wait()
}
|
看完這題目,大家應該就知道是無法使用方式一來讀取 channel 資料,因為 for range 會持續讀資料直到 channel 被關閉為止,這樣是不能保證另一個 gorountine 可以正確讀到下一個字元。
實作方式
從上面範例可以看到兩個 goroutine 裡面寫的代碼應該要一樣,故需要一個 channel 來通知下一個 goroutine 進行讀取,將程式碼改成如下:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| package main
import (
"fmt"
"sync"
)
func main() {
str := []byte("foobar")
ch := make(chan byte, len(str))
next := make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(2)
for i := 0; i < len(str); i++ {
ch <- str[i]
}
close(ch)
go func() {
defer wg.Done()
for {
<-next
v, ok := <-ch
if ok {
fmt.Println("goroutine01:", string(v))
} else {
close(next)
return
}
next <- struct{}{}
}
}()
go func() {
defer wg.Done()
for {
<-next
v, ok := <-ch
if ok {
fmt.Println("goroutine02:", string(v))
} else {
close(next)
return
}
next <- struct{}{}
}
}()
next <- struct{}{}
wg.Wait()
}
|
- 首先當資料全部寫進 Channel 後,需要關閉 Channel
- 新增 next Channel 用來通知下一個 goroutine 讀取資料
- main 主函式要先丟資料到 next Channel
- 當 ch Channel 讀取資料結束後,需要關閉 next Channel
執行完上述步驟後,會得到底下結果
1
2
3
4
5
6
7
| goroutine02: f
goroutine01: o
goroutine02: o
goroutine01: b
goroutine02: a
goroutine01: r
panic: close of closed channel
|
這邊可以看到 <-next 此 channel 被關閉後,會一直有資料,故需要用另一種方式來判斷 channel 是否關閉,就改成如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if ok {
fmt.Println("goroutine01:", string(v))
} else {
close(next)
return
}
next <- stop
}
}()
|
程式可以正確執行了,但是看到 if else 程式碼,我們可以在重構一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if !ok {
close(next)
return
}
fmt.Println("goroutine01:", string(v))
next <- stop
}
}()
|
最後完整程式碼如下,可以線上執行試試看
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| package main
import (
"fmt"
"sync"
)
func main() {
str := []byte("foobar")
ch := make(chan byte, len(str))
next := make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(2)
for i := 0; i < len(str); i++ {
ch <- str[i]
}
close(ch)
go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if !ok {
close(next)
return
}
fmt.Println("goroutine01:", string(v))
next <- stop
}
}()
go func() {
defer wg.Done()
for {
stop, ok := <-next
if !ok {
return
}
v, ok := <-ch
if !ok {
close(next)
return
}
fmt.Println("goroutine02:", string(v))
next <- stop
}
}()
next <- struct{}{}
wg.Wait()
}
|
心得
透過上述範例希望可以讓剛入門朋友了解 Channel 特性,除了此案例之外,大家可以想一下怎麼實現 worker pool pattern,之後有機會可以跟大家介紹此部分。
See also