Go 语言中的 channel 学习

主要学习 Go 语言中 channel 数据结构使用,以及工作过程中的避坑。文章中的代码主要参考极客时间 go 并发编程,如有需要查看其详细内容,请前去购买!

channel 使用方向上的避坑

虽然 Go 的开发者极力推荐使用 channel。但是通过大家的工程化道路上的探索, channel 并不是处理并发问题的普适性的使用方法,有时候使用传统的并发原语更简单,而且不容易出错。

所以在使用并发原语时候,一般遵循以下几种设置方式:

1、共享资源的并发访问使用传统并发原语

2、复杂的任务编排和消息传递使用 channel

3、消息通知机制使用 channel,除非只想 signal 一个 goroutine,才使用 Cond

4、简单等待所有任务的完成用 WaitGroup,也有 channel 的推崇者用 channel,都可以

5、需要和 select 语句结合,使用 channel

6、需要和超时配合时,使用 channel 和 context

channel 具体使用的方式

1、动态处理不定数量的 channel ,使用 reflect.Select 函数,将 channel 当成参数传入,具体案例代码如下:

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

func main() {
var ch1 = make(chan int, 10)
var ch2 = make(chan int, 10)

// 创建SelectCase
var cases = createCases(ch1, ch2)

// 执行10次select
for i := 0; i < 10; i++ {
chosen, recv, ok := reflect.Select(cases)
if recv.IsValid() { // recv case
fmt.Println("recv:", cases[chosen].Dir, recv, ok)
} else { // send case
fmt.Println("send:", cases[chosen].Dir, ok)
}
}
}

func createCases(chs ...chan int) []reflect.SelectCase {
var cases []reflect.SelectCase


// 创建recv case
for _, ch := range chs {
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(ch),
})
}

// 创建send case
for i, ch := range chs {
v := reflect.ValueOf(i)
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectSend,
Chan: reflect.ValueOf(ch),
Send: v,
})
}

return cases
}

上述代码先使用 createCases 函数分别为每个 channel 生成了 recv case 和 send case,并返回一个 reflect.SelectCase 数组。

然后,通过一个循环 10 次的 for 循环执行 reflect.Select 从 cases 中伪随机的选择一个 case 执行。

第一次肯定是 send case,因为此时 channel 还没有元素,recv 还不可用。等 channel 中有了数据以后,recv case 就可以被选择了。这就可以处理不定数量的 channel 。

2、经典的消息传递案例

有 4 个 goroutine,编号为 1、2、3、4。每秒钟会有一个 goroutine 打印出它自己的编号,要求你编写程序,让输出的编号总是按照 1、2、3、4、1、2、3、4……这个顺序打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

type Token struct{}

func newWorker(id int, ch chan Token, nextCh chan Token) {
for {
token := <-ch // 取得令牌
fmt.Println((id + 1)) // id从1开始
time.Sleep(time.Second)
nextCh <- token
}
}
func main() {
chs := []chan Token{make(chan Token), make(chan Token), make(chan Token), make(chan Token)}

// 创建4个worker
for i := 0; i < 4; i++ {
go newWorker(i, chs[i], chs[(i+1)%4])
}

//首先把令牌交给第一个worker
chs[0] <- struct{}{}

select {}
}

首先,我们定义一个令牌类型 Token,其结构为空的 struct,一般都是使用空结构体进行消息的通知!

接着定义一个创建 worker 的方法,这个方法会从它自己的 chan 中读取令牌。哪个 goroutine 取得了令牌,就可以打印出自己编号。

因为需要每秒打印一次数据,所以,我们让它休眠 1 秒后,再把令牌交给它的下家。接着,在第 16 行启动每个 worker 的 goroutine,并在第 20 行将令牌先交给第一个 worker

这样,就会保证程序的运行是 1、2、3、4 的顺序输出。


Go 语言中的 channel 学习
https://chaggle.github.io/2022/05/05/go/concurrency/channel/
作者
chaggle
发布于
2022年5月5日
许可协议