这个系列会介绍 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。
     1 
                    
                    petelin      2019-10-02 11:24:45 +08:00 via iPhone 
                    
                    这样最后一个 data 不还是有可能打不出来么 
                 | 
            
     2 
                    
                    petelin      2019-10-02 11:25:46 +08:00 via iPhone 
                    
                    看错了.. 
                 | 
            
     3 
                    
                    useben      2019-10-02 11:28:16 +08:00 
                    
                    这是你使用有误。一般不是用 context 来通知 chan 写完的,而是关闭 chan,不然可能会造成泄漏。写端应在写完 close chan,读端应检测 chan 再读 chan,chan 返回 false 表明已被关闭,就退出 for 
                 | 
            
     4 
                    
                    guonaihong   OP @useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的作用。有些场景只能用控制和数据分离的写法,个人觉得没有对错之分。 
                 | 
            
     5 
                    
                    guonaihong   OP 写错两个字,纠正下。 
                @useben useben 兄,用的方式是 data chan 既要当数据通道,又要当结束控制通道。上面的例子是控制和数据分离的写法。有些场景只能用控制和数据分离的方法,个人觉得没有对错之分。  | 
            
     6 
                    
                    heimeil      2019-10-02 12:27:31 +08:00 
                    
                    
                 | 
            
     7 
                    
                    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 读数据吗?  | 
            
     8 
                    
                    lishunan246      2019-10-02 12:55:38 +08:00 
                    
                    所以为啥这里要用 context 呢 
                 | 
            
     9 
                    
                    guonaihong   OP @lishunan246  也可以用 done := make(chan struct{}) 这种方式。自从 go1.7 引入 context 之后,现在都用 context 代替 done 的做法。因为很多标准库的参数是 context,后面如果遇到 done 结束还要控制标准库的函数,就不需要修改了。 
                 | 
            
     10 
                    
                    guonaihong   OP @znood 你没有明白代码。无缓存 chan 是生产者,消费者同步的。data<-999 写进入 并且返回了。代表消费者已经消费调了。这时候调用 cancel 是安全的。 
                 | 
            
     11 
                    
                    guonaihong   OP @heimeil 兄弟,我假期用的这台电脑不能翻墙。可否贴下代码,学习下。 
                 | 
            
     12 
                    
                    Nitroethane      2019-10-02 14:57:00 +08:00 via Android 
                    
                    cancel 不能在这个协程函数中调用吧,因为你不能保证在调用 cancel 之前 select 中的第二个 case 把数据读完啊,虽然无缓冲能解决这个问题,但是在实际业务中肯定要用到有缓冲的 channel 吧 
                 | 
            
     13 
                    
                    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) } }  | 
            
     14 
                    
                    guonaihong   OP @znood 这个例子里面不需要 time.After。data chan 消费完。生产者调用 cancel,这时候消费者的 case <- ctx.Done() 就可以返回了。 
                 | 
            
     15 
                    
                    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) }  | 
            
     16 
                    
                    guonaihong   OP @znood 你是想说,如果不用无缓冲 chan。用超时退出? 
                 | 
            
     17 
                    
                    reus      2019-10-02 17:08:53 +08:00    如果看过 go tour 应该都会知道: https://tour.golang.org/concurrency/5 
                 | 
            
     18 
                    
                    such      2019-10-02 17:52:51 +08:00 via iPhone 
                    
                    context 有点滥用了,context 的设计初衷应该是做协程的上下文透传和串联,但是这个例子不涉及到这种场景,都是同一个协程,感觉还是去用另一个 chan 传递退出的信号量 
                 | 
            
     19 
                    
                    guonaihong   OP @such 和 such 兄想得相反,我倒是不觉得滥用。很多时候一个技术被滥用是带来了性能退化,这里没有性能退化。再者 context 源码里面也是 close chan 再实现通知的。和自己 close chan 来没啥区别。 
                 | 
            
     20 
                    
                    guonaihong   OP @reus 感谢分享。 
                 | 
            
     21 
                    
                    guonaihong   OP @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) } } } ```  | 
            
     22 
                    
                    heimeil      2019-10-02 22:40:32 +08:00 
                    
                    并不是判断为空的意思,你可以这样试试看: 
                case <-ctx.Done(): if len(data) == 0 { fmt.Println("Done") return } else { fmt.Println("--------------") }  | 
            
     23 
                    
                    znood      2019-10-03 16:45:50 +08:00 
                    
                    肯定要用带缓冲的,不带缓冲的两遍阻塞用两个协程没有意义,用一个协程就处理了 
                 |