golang 常见坑(1)-select

2019-10-02 10:57:57 +08:00
 guonaihong

这个系列会介绍 golang 常见的坑。当然很多坑是由于对 golang 理解不到位引起的。

猜测下如下代码的输出

这是一段很简单的代码,生产者 go 程打印数字,结束之后发送 cancel 信号。 是不是认为会打印 0-999。如果是这样想的可以继续往下看。

package main

import (
        "context"
        "fmt"
)

func main() {
        ctx, cancel := context.WithCancel(context.Background())

        data := make(chan int, 1000)

        go func() {
                for i := 0; i < 1000; i++ {
                        data <- i
                }
                cancel()
        }()

        for {
                select {
                case <-ctx.Done():
                        return
                case v := <-data:
                        fmt.Printf("%d\n", v)
                }
        }
}

问题分析

你以为会打印 0-999 ?其实不是。。运行下代码你会发现。输出是随机的。what? 这其实和 select 的机制有关系。当 case 条件有多个为真,就想象成随机函数从 case 里面选择一个执行 。上面的代码是两个条件都满足,调用 cancel 函数,有些数据还缓存在 data chan 里面,ctx.Done()条件也为真。选择到 ctx.Done()的时候,这里很可能 case v:=<-data 都没打印全。

解决问题

刚刚聊了 case 的内部逻辑。再聊下如何解决这个问题。data 每个发送的数据都确保消费掉,最后再调用 cancel 函数就可解决这个问题。做法把带缓冲的 chan 修改为不带缓冲。

// data := make(chan int, 1000)
data := make(chan int)

最佳实践

如果不是必须的理由要用带缓冲的 chan。推荐使用无缓冲的 chan。至于担心的性能问题,他们性能差距不大。后面会补上 benchmark。

我的 github

https://github.com/guonaihong/gout

5557 次点击
所在节点    Go 编程语言
24 条回复
petelin
2019-10-02 11:24:45 +08:00
这样最后一个 data 不还是有可能打不出来么
petelin
2019-10-02 11:25:46 +08:00
看错了..
useben
2019-10-02 11:28:16 +08:00
这是你使用有误。一般不是用 context 来通知 chan 写完的,而是关闭 chan,不然可能会造成泄漏。写端应在写完 close chan,读端应检测 chan 再读 chan,chan 返回 false 表明已被关闭,就退出 for
guonaihong
2019-10-02 11:33:30 +08:00
@useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的作用。有些场景只能用控制和数据分离的写法,个人觉得没有对错之分。
guonaihong
2019-10-02 11:35:08 +08:00
写错两个字,纠正下。
@useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的写法。有些场景只能用控制和数据分离的方法,个人觉得没有对错之分。
heimeil
2019-10-02 12:27:31 +08:00
context 的设计目的就是尽早结束、释放资源的,你想要保证 channel 被读完的话,就需要再做一些处理

https://play.golang.org/p/jKLArlvONhM
znood
2019-10-02 12:55:05 +08:00
这不能叫 Golang 有坑,只能叫你 Golang 没学好

context 对 select 做中断处理不管你有没有执行完才是正常情况,如果想要处理完就用其他方法比如三楼的方法

再说你对这个处理的问题
// data := make(chan int, 1000)
data := make(chan int)
你以为这样就能保证万无一失了吗?你也说了 select 是随机的,但是如果把 fmt.Printf("%d\n", v)换成处理时间长的,这个时候 data <- 999 放进去了,cancel()也执行了,你觉得 select 是一定会选择从 data 读数据吗?
lishunan246
2019-10-02 12:55:38 +08:00
所以为啥这里要用 context 呢
guonaihong
2019-10-02 14:20:55 +08:00
@lishunan246 也可以用 done := make(chan struct{}) 这种方式。自从 go1.7 引入 context 之后,现在都用 context 代替 done 的做法。因为很多标准库的参数是 context,后面如果遇到 done 结束还要控制标准库的函数,就不需要修改了。
guonaihong
2019-10-02 14:23:13 +08:00
@znood 你没有明白代码。无缓存 chan 是生产者,消费者同步的。data<-999 写进入 并且返回了。代表消费者已经消费调了。这时候调用 cancel 是安全的。
guonaihong
2019-10-02 14:30:49 +08:00
@heimeil 兄弟,我假期用的这台电脑不能翻墙。可否贴下代码,学习下。
Nitroethane
2019-10-02 14:57:00 +08:00
cancel 不能在这个协程函数中调用吧,因为你不能保证在调用 cancel 之前 select 中的第二个 case 把数据读完啊,虽然无缓冲能解决这个问题,但是在实际业务中肯定要用到有缓冲的 channel 吧
znood
2019-10-02 15:06:46 +08:00
好吧,献丑了,忘了无缓存 channel 是阻塞的了

不过这里用 cancel 肯定是不合适的,因为你想把队列读取完,又不想关闭 channel,这个时候用 time.After,ctx 无条件返回,读取 channel 超时(队列空)返回
for {
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
return
case v := <-data:
fmt.Printf("%d\n", v)
}
}
guonaihong
2019-10-02 15:59:10 +08:00
@znood 这个例子里面不需要 time.After。data chan 消费完。生产者调用 cancel,这时候消费者的 case <- ctx.Done() 就可以返回了。
heimeil
2019-10-02 16:03:15 +08:00
package main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())

data := make(chan int, 10)

go func() {
for i := 0; i < 10; i++ {
data <- i
}
cancel()
fmt.Println("cancel")
}()

for {
select {
case <-ctx.Done():
fmt.Println("Done")
return
case v := <-data:
doSomething(v)
RL:
for {
select {
case v := <-data:
doSomething(v)
default:
break RL
}
}
}
}
}

func doSomething(v int) {
time.Sleep(time.Millisecond * 100)
fmt.Println(v)
}
guonaihong
2019-10-02 16:49:10 +08:00
@znood 你是想说,如果不用无缓冲 chan。用超时退出?
reus
2019-10-02 17:08:53 +08:00
如果看过 go tour 应该都会知道: https://tour.golang.org/concurrency/5
such
2019-10-02 17:52:51 +08:00
context 有点滥用了,context 的设计初衷应该是做协程的上下文透传和串联,但是这个例子不涉及到这种场景,都是同一个协程,感觉还是去用另一个 chan 传递退出的信号量
guonaihong
2019-10-02 21:39:55 +08:00
@such 和 such 兄想得相反,我倒是不觉得滥用。很多时候一个技术被滥用是带来了性能退化,这里没有性能退化。再者 context 源码里面也是 close chan 再实现通知的。和自己 close chan 来没啥区别。
guonaihong
2019-10-02 21:40:19 +08:00
@reus 感谢分享。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/605974

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX