Golang Go语言中常见坑(1)-select

发布于 1周前 作者 sinazl 来自 Go语言

Golang Go语言中常见坑(1)-select

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


更多关于Golang Go语言中常见坑(1)-select的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

25 回复

这样最后一个 data 不还是有可能打不出来么

更多关于Golang Go语言中常见坑(1)-select的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


看错了…

这是你使用有误。一般不是用 context 来通知 chan 写完的,而是关闭 chan,不然可能会造成泄漏。写端应在写完 close chan,读端应检测 chan 再读 chan,chan 返回 false 表明已被关闭,就退出 for

useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的作用。有些场景只能用控制和数据分离的写法,个人觉得没有对错之分。

写错两个字,纠正下。
useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的写法。有些场景只能用控制和数据分离的方法,个人觉得没有对错之分。

context 的设计目的就是尽早结束、释放资源的,你想要保证 channel 被读完的话,就需要再做一些处理

https://play.golang.org/p/jKLArlvONhM

这不能叫 Golang 有坑,只能叫你 Golang 没学好

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

再说你对这个处理的问题
// data := make(chan int, 1000)
data := make(chan int)
你以为这样就能保证万无一失了吗?你也说了 select 是随机的,但是如果把 fmt.Printf("%d\n", v)换成处理时间长的,这个时候 data <- 999 放进去了,cancel()也执行了,你觉得 select 是一定会选择从 data 读数据吗?

所以为啥这里要用 context 呢

也可以用 done := make(chan struct{}) 这种方式。自从 go1.7 引入 context 之后,现在都用 context 代替 done 的做法。因为很多标准库的参数是 context,后面如果遇到 done 结束还要控制标准库的函数,就不需要修改了。

你没有明白代码。无缓存 chan 是生产者,消费者同步的。data<-999 写进入 并且返回了。代表消费者已经消费调了。这时候调用 cancel 是安全的。

兄弟,我假期用的这台电脑不能翻墙。可否贴下代码,学习下。

cancel 不能在这个协程函数中调用吧,因为你不能保证在调用 cancel 之前 select 中的第二个 case 把数据读完啊,虽然无缓冲能解决这个问题,但是在实际业务中肯定要用到有缓冲的 channel 吧

好吧,献丑了,忘了无缓存 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)
}
}

这个例子里面不需要 time.After。data chan 消费完。生产者调用 cancel,这时候消费者的 case <- ctx.Done() 就可以返回了。

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)
}

你是想说,如果不用无缓冲 chan。用超时退出?

如果看过 go tour 应该都会知道: https://tour.golang.org/concurrency/5

context 有点滥用了,context 的设计初衷应该是做协程的上下文透传和串联,但是这个例子不涉及到这种场景,都是同一个协程,感觉还是去用另一个 chan 传递退出的信号量

和 such 兄想得相反,我倒是不觉得滥用。很多时候一个技术被滥用是带来了性能退化,这里没有性能退化。再者 context 源码里面也是 close chan 再实现通知的。和自己 close chan 来没啥区别。

如果 chan 是带缓冲的,并且因为某些原因不能修改为无缓冲的,可以用下面的该法。你的代码我看了,用两层 for 循环的做法,本质还是想知道 chan 有没有空。直接用个判断就行。

go<br>ackage main<br><br>import (<br> "context"<br> "fmt"<br> "time"<br>)<br><br>func main() {<br> ctx, cancel := context.WithCancel(context.Background())<br><br> data := make(chan int, 10)<br><br> go func() {<br> for i := 0; i &lt; 10; i++ {<br> data &lt;- i<br> }<br> cancel()<br> fmt.Println("cancel")<br> }()<br><br> for {<br> select {<br> case &lt;-ctx.Done():<br> if len(data) == 0 {<br> fmt.Println("Done")<br> return<br> }<br> case v := &lt;-data:<br> fmt.Printf("v = %d\n", v)<br> }<br> }<br>}<br><br>

并不是判断为空的意思,你可以这样试试看:
case <-ctx.Done():
if len(data) == 0 {
fmt.Println(“Done”)
return
} else {
fmt.Println("--------------")
}

肯定要用带缓冲的,不带缓冲的两遍阻塞用两个协程没有意义,用一个协程就处理了

#23 不不不,有意义,chan 的重点是对程序逻辑的拆解(或者说通过加一层抽象解决复杂问题),而且很多时候并非性能热点,chan 的阻塞操作性能还比有缓冲的高不少)虽然肯定没直接一个的快)
就像很多时候明明可以复制粘贴,为啥要写一个函数呢,这里 chan 的作用就在于此,在合适的地方拆分模块,复用代码,降低耦合性,并不是所有场景都能用回调解决

在Go语言中,select语句是用于处理多个通道(channel)操作的,它类似于switch语句,但专门用于通道通信。在使用select时,开发者需要注意以下几个常见的坑:

  1. case分支:如果你希望在没有其他通道准备好时执行某个操作,可以添加一个默认的空case分支。如果没有这样的分支,而所有通道都未准备好,select会阻塞。

  2. 通道关闭与零值:从已关闭的通道接收数据时,会立即返回该类型的零值,而不会阻塞。因此,在select中使用已关闭的通道时,要特别注意区分是通道正常关闭还是发生了其他错误。

  3. 多个相同条件的caseselect中的多个case可以监听同一个通道,但在每次select执行时,只有一个满足条件的case会被执行(如果有多个相同条件的case都准备好)。

  4. 通道发送操作的select:在select中也可以包含发送操作,但同样要注意通道的关闭状态,避免向已关闭的通道发送数据导致panic。

  5. selectfor循环:通常select会放在for循环中,以持续监听通道事件。在编写这样的代码时,要确保循环的退出条件清晰,避免无限循环。

总之,select是Go语言中处理并发通信的强大工具,但使用时需要小心谨慎,以避免上述常见坑。通过合理设计代码结构,可以有效利用select实现高效的并发控制。

回到顶部