大家好,我是polarisxu。前两天火顶笔记发表了一篇文章:《一个 select 死锁问题》[1],又是一个小细节。我将更改问题以便更好地理解:packagemainimport"sync"funcmain(){varwgsync.WaitGroupfoo:=make(chanint)bar:=make(chanint)wg.Add(1)gofunc(){deferwg.Done()select{casefoo<-<-bar:default:println("default")}}()wg.Wait()}按照惯例,gofunc中的select应该执行default分支,程序正常运行。但结果不是,而是陷入僵局。您可以通过此链接对其进行测试:https://play.studygolang.com/p/kF4pOjYXbXf。文章也解释了原因。Go语言规范中有这样一句话:对于语句中的所有情况,接收操作的通道操作数和发送语句的通道和右侧表达式只计算一次,按源顺序,在进入“选择”语句。结果是一组要从中接收或发送到的通道,以及要发送的相应值。无论选择哪个(如果有的话)通信操作继续进行,该评估中的任何副作用都会发生。RecvStmt左侧带有简短变量声明或赋值的表达式尚未计算。不知道大家看懂了吗?那么,最后来个例子来验证你是否理解:为什么每次都是输出一半?数据,然后死锁?(同样,你可以在这里运行查看结果:https://play.studygolang.com/p/zoJtTzI7K5T)chanstring)gofunc(){fori:=0;i<5;i++{ch<-fmt.Sprintf("%s%d",msg,i)time.Sleep(time.Duration(sleep)*time.Millisecond)}}()返回urnch}funcfanIn(input1,input2<-chanstring)<-chanstring{ch:=make(chanstring)gofunc(){for{select{casech<-<-input1:casech<-<-input2:}}}()返回}funcmain(){ch:=fanIn(talk("A",10),talk("B",1000))fori:=0;i<10;i++{fmt.Printf("%q\n",<-ch)}}有没有这种感觉:算法介绍这是StackOverflow上的一道题:https://stackoverflow.com/questions/51167940/chained-channel-operations-in-a-single-select-casekey的点和文章开头的例子一样,就是selectcase中的两个channel连在一起,也就是fanIn函数中:select{casech<-<-input1:casech<-<-input2:}改成这样就一切正常了:select{caset:=<-input1:ch<-tcaset:=<-input2:ch<-t}分析一下Go语言规范中的那句话这个更复杂的例子。对于select语句,每个case子句在输入语句时按源顺序进行评估:此评估仅适用于发送或接收操作的额外表达式。例如://ch是一个chanint;//getVal()返回一个int//input是一个chanint//getch()返回一个chanintselect{casech<-getVal():casech<-<-input:casegetch()<-1:case<-getch():}在选择具体的case执行之前,示例中的getVal()、<-input和getch()都会被执行。这里有一个验证的例子:https://play.studygolang.com/p/DkpCq3aQ1TE。packagemainimport("fmt")funcmain(){ch:=make(chanint)gofunc(){select{casech<-getVal(1):fmt.Println("infirstcase")casech<-getVal(2):fmt.Println("insecondcase")default:fmt.Println("default")}}()fmt.Println("Theval:",<-ch)}funcgetVal(iint)int{fmt.Println("getVal,i=",i)returni}无论select最终选择了哪种情况,getVal()都会按照源码的顺序执行:getVal(1)和getVal(2),即必须先输出:getVal,i=1getVal,i=2大家可以仔细想想。现在回到StackOverflow上的那个问题。每次输入如下select语句:select{casech<-<-input1:casech<-<-input2:}<-input1和<-input2都会被执行,对应的值为:Ax和Bx(其中x为0-5)。但是每次select只会选择其中一个case执行,所以<-input1和<-input2的结果其中一个必须舍弃掉,即不写入ch。所以总共只会输出5次,其他5次的结果都会丢失。(你会发现5个输出结果中,x例如是01234)而main中循环10次,只得到5个结果,所以输出5次后就报死锁。虽然这是一个小细节,但在实际开发中还是可以的。比如文章中写的例子://chisachanint;//getVal()返回int//输入是chanint//getch()返回chanintselect{casech<-getVal():casech<-<-input:casegetch()<-1:case<-getch():}所以在使用select的时候,一定要注意这个可能出现的问题。不要以为这个问题不会遇到,其实很常见。最常见的问题是time.After导致内存泄漏。网上有很多文章解释原因以及如何避免。其实最根本的原因还是select机制。比如下面的代码有内存泄漏(传给time.after的时间参数越大,泄漏越严重),能解释一下原因吗?packagemainimport("time")funcmain(){ch:=make(chanint,10)gofunc(){vari=1for{i++ch<-i}}()for{select{caseex:=<-ch:println(x)case<-time.After(30*time.Second):println(time.Now().Unix())}}}参考文献[1]《一个 select 死锁问题》:https://blog.huoding.com/2021/08/29/947本文转载自微信?「polarisxu」,可通过以下二维码关注。转载本文请联系polarisxu公众号。
