go语言select语句中的求值问题

2021-09-23 11:33:09 浏览数 (2)

1. 从一个问题说起

代码语言:javascript复制
package main

import (
	"fmt"
)

var ch0 = make(chan int)
var ch1 = make(chan int)
var chs = [](chan int){ch0, ch1}

func getCh(i int) chan int{
	fmt.Printf("get ch:%dn", i)
	return chs[i]
}

func getNum(i int) int{
	fmt.Printf("get num:%dn", i)
	return i
}

func main(){
	select {
		case getCh(0)<- getNum(0):
			fmt.Println("case 0")
		case getCh(1)<- getNum(1):
			fmt.Println("case 1")
		default:
			fmt.Println("default")
	}
}

上面这段代码输出是什么?

如果你的答案是case 0, case 1随机出现,那么,请接着往下看。

2. 答案

题目的输出是这样的

代码语言:javascript复制
get ch:0
get num:0
get ch:1
get num:1
default

题目涉及两个知识点:

  1. 对于无缓冲的channel,如果接收方未准备好,则发送操作将会被阻塞。因此上面的代码才会走到default分支。
  2. select中,所有case中的语句会被求值。这也是为什么明明走到了default,但getCh(0), getCh(1), getNum(0), getNum(1), 都会被执行。下一小节中我们会着重阐述这个问题。

3. select语句中的求值

手册中的说明是这样的:

For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the “select” statement. (更多详情点击这里)

这段话,被好多文章翻译为:

所有channel表达式都会被求值, 所有被发送的表达式都会被求值。求值顺序:自上而下、从左到右。

这…这是欺负我不懂英文么…如此翻译,隐去了太多细节!要想理解这段话,我们用下图来对齐下概念:

需要说明的是,receive operation可以只是<-ch

如此一来,这段话就行好理解了。对于select语句中的所有case,图中1,2的ch部分和3的expression部分都会被进行一次求值。求值顺序为代码顺序。

其重点在于,无论相应的case是被选中,求值都会被执行!正如手册中所说

Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed.

至此,相应你应该可以明白getCh(), getNum()输出的原因了。

4. 更进一步

如果确认了解了上面的知识点,我们来看下面的代码,输出是什么?

代码语言:javascript复制
package main

import (
	"fmt"
)

var ch1 = make(chan int)
var ch2 = make(chan int)

func main(){
	select {
		case ch1 <- <-ch2:
			fmt.Println("case 0")
		default:
			fmt.Println("default")
	}
}

答案是死锁了。

原因是这样的<-ch2被作为发送语句ch1 <- <-ch2的右值被整体求值。但<-ch2本身是阻塞状态,无法求值,自然也无法进行select后面的执行步骤,因此死锁。这可能也是手册中所说的求值的副作用之一吧。

如果想解除死锁,简单修改下select部分即可。

代码语言:javascript复制
	select {
    	case v:= <-ch2:
    		ch1 <- v
    		fmt.Println("case 0")
    	default:
    		fmt.Println("default")
	}

推荐阅读:

  • 手册Select_statements
  • 手册Send statements
  • 一个 select 死锁问题

0 人点赞