之前的博客中已经粗略探究了一下 golang channel 的实现原理,本文总结一下使用 channel 的各种姿势。
先看一下对不同状态的 channel 的读,写,关闭操作的结果
1. 使用 for range 读取 channel
场景: 当需要不断从 channel 里读数据时
这是最常用的方式,又安全又便利,当channel 被关闭时,for 循环自动退出。 用法不再赘述。
2. 使用 _, ok
判断 channel 是否关闭
场景: 读 channel,但需要判断 channel 是否已关闭。
读 channel 的操作 <- chan
既可以返回一个值,也可以返回两个值,这里就是用的两个返回值的方式。
举例:
if v, ok := <- ch; ok {
// can read channel
fmt.Println(v)
}
- 读到数据,并且通道没有关闭时,ok 的值为
true
。 - 通道关闭,无数据读到时,ok 的值为
false
。
3. 与 select 搭配使用
场景: 需要对多个通道进行处理,或者设置超时
举例:
func (h *Handler) handle(job *Job) {
select {
case h.jobCh<-job:
return
case <-h.stopCh:
return
case <- time.After(2):
}
}
这里有一个坑需要注意: 当 channel 为 nil
,也就是没有初始化时,无论对应的 select case 是读还是写,都会立即返回。 而一般情况下,对 nil channel 的 写操作是要panic的。
另一个技巧是 select 的时候配置 default,实现 非阻塞 channel , 这一用法在 channel 的实现中也提及。
select {
// 如果chan1成功读到数据,则进行该case处理语句
case <- chan1:
// 如果成功向chan2写入数据,则进行该case处理语句
case chan2 <- 1:
// 如果上面都没有成功,则进入default处理流程
default:
}
另外,上面两个例子都提到了 case chan2 <- 1
这样的用法,这个用法表示:如果能向channel 写入数据,则进行该 case 的处理。
4. 选择性的返回 chan 为只读或只写
这算是一个编程技巧,当把 channel 变量作为参数或者函数返值时,
如果参数定义是 <-chan int
, 那么对这个 channel 变量只有读权限,无法写入数据。
同理,如果参数定义是 chan <- int
,那么对这个 channel 变量只有写权限,无法读数据,否则编译报错。
举例,Golang的标准库里就有很多例子,比如 time.After(), context.Done() 等
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
除此之外,另举一个例子
func main() {
c := make(chan int)
go send(c)
go recv(c)
}
//只能向chan里写数据
func send(c chan<- int) {
for i := 0; i < 10; i++ {
c <- i
}
}
//只能取channel中的数据
func recv(c <-chan int) {
for i := range c {
fmt.Println(i)
}
}
5. 使用 close(ch) 关闭所有 goroutine
问题: 假设有 100 个 goroutine,我想让它们同时退出该怎么办?
答案是 close(channel), 而不是往 chan 里发数据,发数据每次只会有一个 goroutine 会受到,如果要关闭 100 个,那么要发100 次。 而关闭一个 chan 后,所有的 goroutine 会同时读到0值(见文章开头图表)。
func function(id int, wg *sync.WaitGroup, quit <-chan int) {
defer wg.Done()
select {
case <- quit:
fmt.Println(id, "goroutine exit")
return
}
}
func main() {
quit := make(chan int)
wg := &sync.WaitGroup{}
for i := 0; i < 10; i ++ {
wg.Add(1)
go function(i, wg, quit)
}
fmt.Println("10 gorutine start...")
time.Sleep(time.Second*3)
close(quit) // 关闭 chan,所有 goroutine 退出
wg.Wait()
}
6. nil 的 channel
在第 3 小节中已经提到了 nil 的 channel,但是只是教条的记住了语法,仍然不知道怎么用,这里通过一个场景来了解 select 和 nil 的用法。
问题: 有一个 goroutine 中 select 的 case 有多个 channel,我希望所有 case 的chan 都关闭后,goroutine才能退出
答案就是利用 select 不会在 nil 的 channel 上进行等待,因此将channel赋值为nil即可。此外,还需要利用channel的ok值。
var wg sync.WaitGroup
func worker(in1, in2 <-chan int) {
defer wg.Done()
for {
select {
case _, ok := <-in1:
if !ok {
fmt.Println("收到退出信号 1")
in1 = nil
}
case _, ok := <-in2:
if !ok {
fmt.Println("收到退出信号 2")
in2 = nil
}
}
// 都为nil则结束当前goroutine
if in1 == nil && in2 == nil {
return
}
}
}
func main() {
in1 := make(chan int)
in2 := make(chan int)
wg.Add(2)
go worker(in1, in2)
go worker(in2, in2)
close(in1)
time.Sleep(time.Second)
close(in2)
wg.Wait()
}
不过执行上面的代码最后看到如下输出,按理说应该 1 和 2 各打印一条 log,为什么实际是 4 条呢 ?
收到退出信号 1
收到退出信号 1
收到退出信号 2
收到退出信号 2
总结
channel 是 golang 语言的精髓之一,灵活把 select, channel 和 goroutine 结合起来使用可以达到事半功倍的效果。
在使用 channel 时:
- 发送协程主动关闭 chan,接收协程不关闭 chan。 把接收方的通道入参声明为只读(<-chan),如果接收协程关闭只读协程,编译时就会报错。
- gorutine 处理1个 channel,并且是读时,goroutine 优先使用
for-range
,因为range可以在 chan 关闭时自动退出循环。 _, ok
可以处理多个读通道关闭,需要关闭当前使用for-select的协程。