Golang Go语言中常见坑(1)-select
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
更多关于Golang Go语言中常见坑(1)-select的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这样最后一个 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 吧
这个例子里面不需要 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)
}
如果看过 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 < 10; i++ {<br> data <- i<br> }<br> cancel()<br> fmt.Println("cancel")<br> }()<br><br> for {<br> select {<br> case <-ctx.Done():<br> if len(data) == 0 {<br> fmt.Println("Done")<br> return<br> }<br> case v := <-data:<br> fmt.Printf("v = %d\n", v)<br> }<br> }<br>}<br><br>
肯定要用带缓冲的,不带缓冲的两遍阻塞用两个协程没有意义,用一个协程就处理了
#23 不不不,有意义,chan 的重点是对程序逻辑的拆解(或者说通过加一层抽象解决复杂问题),而且很多时候并非性能热点,chan 的阻塞操作性能还比有缓冲的高不少)虽然肯定没直接一个的快)
就像很多时候明明可以复制粘贴,为啥要写一个函数呢,这里 chan 的作用就在于此,在合适的地方拆分模块,复用代码,降低耦合性,并不是所有场景都能用回调解决
在Go语言中,select
语句是用于处理多个通道(channel)操作的,它类似于switch语句,但专门用于通道通信。在使用select
时,开发者需要注意以下几个常见的坑:
-
空
case
分支:如果你希望在没有其他通道准备好时执行某个操作,可以添加一个默认的空case
分支。如果没有这样的分支,而所有通道都未准备好,select
会阻塞。 -
通道关闭与零值:从已关闭的通道接收数据时,会立即返回该类型的零值,而不会阻塞。因此,在
select
中使用已关闭的通道时,要特别注意区分是通道正常关闭还是发生了其他错误。 -
多个相同条件的
case
:select
中的多个case
可以监听同一个通道,但在每次select
执行时,只有一个满足条件的case
会被执行(如果有多个相同条件的case
都准备好)。 -
通道发送操作的
select
:在select
中也可以包含发送操作,但同样要注意通道的关闭状态,避免向已关闭的通道发送数据导致panic。 -
select
与for
循环:通常select
会放在for
循环中,以持续监听通道事件。在编写这样的代码时,要确保循环的退出条件清晰,避免无限循环。
总之,select
是Go语言中处理并发通信的强大工具,但使用时需要小心谨慎,以避免上述常见坑。通过合理设计代码结构,可以有效利用select
实现高效的并发控制。