golang 常见坑(1)-select - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
guonaihong

golang 常见坑(1)-select

  •  
  •   guonaihong
    guonaihong Oct 2, 2019 6224 views
    This topic created in 2401 days ago, the information mentioned may be changed or developed.

    这个系列会介绍 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

    Supplement 1    Oct 2, 2019
    ### 后续
    给位 v 友都很热情。其实上面的例子,只想表达一些观点。
    * select 监听多个 case 的时候,有多个 case 条件都满足的话,是随机选择一个执行的。这里再叨叨下,是从满足的条件里面。。。
    * 无缓冲 chan 是生产者消费者同步的,这点很多人会忽略,细细品味,所以例子最后的改法才是这样的。
    * 为什么用 context,其实也可以用 done := make(chan struct{}),代替 context。只是 go1.7 之后 context 就被扶正,使用 context 可以和标准库里面很多函数打通,如果一开始用 done,后面还要修改参数。
    * 为啥不直接一个 data,生产者不停 data<- i。for 循环结束,直接 close(data)。这种方式是对的,有什么好讲的。。。。
    24 replies    2019-10-03 17:41:47 +08:00
    petelin
        1
    petelin  
       Oct 2, 2019 via iPhone
    这样最后一个 data 不还是有可能打不出来么
    petelin
        2
    petelin  
       Oct 2, 2019 via iPhone
    看错了..
    useben
        3
    useben  
       Oct 2, 2019
    这是你使用有误。一般不是用 context 来通知 chan 写完的,而是关闭 chan,不然可能会造成泄漏。写端应在写完 close chan,读端应检测 chan 再读 chan,chan 返回 false 表明已被关闭,就退出 for
    guonaihong
        4
    guonaihong  
    OP
       Oct 2, 2019
    @useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的作用。有些场景只能用控制和数据分离的写法,个人觉得没有对错之分。
    guonaihong
        5
    guonaihong  
    OP
       Oct 2, 2019
    写错两个字,纠正下。
    @useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的写法。有些场景只能用控制和数据分离的方法,个人觉得没有对错之分。
    heimeil
        6
    heimeil  
       Oct 2, 2019
    context 的设计目的就是尽早结束、释放资源的,你想要保证 channel 被读完的话,就需要再做一些处理

    https://play.golang.org/p/jKLArlvONhM
    znood
        7
    znood  
       Oct 2, 2019
    这不能叫 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
        8
    lishunan246  
       Oct 2, 2019
    所以为啥这里要用 context 呢
    guonaihong
        9
    guonaihong  
    OP
       Oct 2, 2019
    @lishunan246 也可以用 done := make(chan struct{}) 这种方式。自从 go1.7 引入 context 之后,现在都用 context 代替 done 的做法。因为很多标准库的参数是 context,后面如果遇到 done 结束还要控制标准库的函数,就不需要修改了。
    guonaihong
        10
    guonaihong  
    OP
       Oct 2, 2019
    @znood 你没有明白代码。无缓存 chan 是生产者,消费者同步的。data<-999 写进入 并且返回了。代表消费者已经消费调了。这时候调用 cancel 是安全的。
    guonaihong
        11
    guonaihong  
    OP
       Oct 2, 2019
    @heimeil 兄弟,我假期用的这台电脑不能翻墙。可否贴下代码,学习下。
    Nitroethane
        12
    Nitroethane  
       Oct 2, 2019 via Android
    cancel 不能在这个协程函数中调用吧,因为你不能保证在调用 cancel 之前 select 中的第二个 case 把数据读完啊,虽然无缓冲能解决这个问题,但是在实际业务中肯定要用到有缓冲的 channel 吧
    znood
        13
    znood  
       Oct 2, 2019
    好吧,献丑了,忘了无缓存 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
        14
    guonaihong  
    OP
       Oct 2, 2019
    @znood 这个例子里面不需要 time.After。data chan 消费完。生产者调用 cancel,这时候消费者的 case <- ctx.Done() 就可以返回了。
    heimeil
        15
    heimeil  
       Oct 2, 2019
    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
        16
    guonaihong  
    OP
       Oct 2, 2019
    @znood 你是想说,如果不用无缓冲 chan。用超时退出?
    reus
        17
    reus  
       Oct 2, 2019   2
    如果看过 go tour 应该都会知道: https://tour.golang.org/concurrency/5
    such
        18
    such  
       Oct 2, 2019 via iPhone
    context 有点滥用了,context 的设计初衷应该是做协程的上下文透传和串联,但是这个例子不涉及到这种场景,都是同一个协程,感觉还是去用另一个 chan 传递退出的信号量
    guonaihong
        19
    guonaihong  
    OP
       Oct 2, 2019
    @such 和 such 兄想得相反,我倒是不觉得滥用。很多时候一个技术被滥用是带来了性能退化,这里没有性能退化。再者 context 源码里面也是 close chan 再实现通知的。和自己 close chan 来没啥区别。
    guonaihong
        20
    guonaihong  
    OP
       Oct 2, 2019
    @reus 感谢分享。
    guonaihong
        21
    guonaihong  
    OP
       Oct 2, 2019
    @heimeil 如果 chan 是带缓冲的,并且因为某些原因不能修改为无缓冲的,可以用下面的该法。你的代码我看了,用两层 for 循环的做法,本质还是想知道 chan 有没有空。直接用个判断就行。

    ```go
    ackage 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():
    if len(data) == 0 {
    fmt.Println("Done")
    return
    }
    case v := <-data:
    fmt.Printf("v = %d\n", v)
    }
    }
    }

    ```
    heimeil
        22
    heimeil  
       Oct 2, 2019
    并不是判断为空的意思,你可以这样试试看:
    case <-ctx.Done():
    if len(data) == 0 {
    fmt.Println("Done")
    return
    } else {
    fmt.Println("--------------")
    }
    znood
        23
    znood  
       Oct 3, 2019
    肯定要用带缓冲的,不带缓冲的两遍阻塞用两个协程没有意义,用一个协程就处理了
    codehz
        24
    codehz  
       Oct 3, 2019
    @znood #23 不不不,有意义,chan 的重点是对程序逻辑的拆解(或者说通过加一层抽象解决复杂问题),而且很多时候并非性能热点,chan 的阻塞操作性能还比有缓冲的高不少)虽然肯定没直接一个的快)
    就像很多时候明明可以复制粘贴,为啥要写一个函数呢,这里 chan 的作用就在于此,在合适的地方拆分模块,复用代码,降低耦合性,并不是所有场景都能用回调解决
    About     Help     Advertise     Blog     API     FAQ     Solana     3672 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 63ms UTC 10:34 PVG 18:34 LAX 03:34 JFK 06:34
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86