Go 語言強大的 goroutine 特性 ,讓各位開發者愛不釋手,而多個 goroutine 如何溝通呢?就是透過 Channel 來做到。本篇教大家從 Channel 讀取資料的兩種方式及使用時機,並實際用一個案例快速了解 Channel 實作上會遇到哪些問題?底下用兩個範例讓大家了解如何讀取 Channel 資料出來。
影片教學 VIDEO
其他線上課程請參考如下
讀取 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