表达式估值顺序规则

本文将解释各种情形下表达式的估值顺序。

一个表达式将在其所依赖的其它表达式估值之后进行估值

这属于常识,没什么好解释的。一个显然的例子是一个表达式将在组成它的子表达式都估值之后才能进行估值。比如,在一个函数调用f(x, y[n])中,

  • f()将在它所依赖的子表达式fxy[n]估值之后进行估值;
  • 表达式y[n]将在它所依赖的子表达式ny估值之后进行估值。 另外,程序资源初始化顺序章节中提供了一个关于包级变量初始化顺序的例子。

包级变量初始化顺序

在运行时刻,当一个包被加载的时候,不依赖于任何其它未初始化包级变量的未初始化包级变量将按照它们在代码中的声明顺序被初始化。此过程可能需要重复若干次,直到此过程中不再有任何包级变量被初始化。对于一个成功编译了的Go程序,当所有这样的过程结束之后,所有的包级变量都应该被初始化了。

在包级变量初始化过程中,呈现为空标识符的包级变量和其它包级变被同等对待。 比如,下面这个程序应该打印出yzxw

  • 在上述过程运行的第一轮,yz是仅有的两个不依赖于任何其它未初始化包级变量的未初始化包级变量,所以它们将按照它们的声明顺序被初始化。
  • 在上述过程运行的第二轮,x是仅有的一个不依赖于任何其它未初始化包级变量的未初始化包级变量,所以它将被初始化。
  • 在上述过程运行的第三轮,w是仅有的一个不依赖于任何其它未初始化包级变量的未初始化包级变量,所以它将被初始化。
  1. package main
  2. var (
  3. _ = f("w", x)
  4. x = f("x", z)
  5. y = f("y")
  6. z = f("z")
  7. )
  8. func f(s string, deps ...int) int {
  9. print(s)
  10. return 0
  11. }
  12. func main() {
  13. f("\n")
  14. }

(注意:标准Go编译器1.13之前的版本并未正确地实现上述规则。目前,如果上面这个程序使用标准Go编译器1.12版本编译的话,它将打印出zxwy。) !--https://github.com/golang/go/issues/31292https://github.com/golang/go/commit/451cf3e2cd8950571f436896a3987343f8c2d7f6-- 通过一个多值表达式源值来初始化的多个包级变量将被一起初始化。比如,在包级变量声明var x, y = f()中,变量xy将被一起初始化。或者说,在它们之间不会有其它包级变量被初始化。 如果一些包级变量之间存在着编译器难以觉察的依赖关系,则这些包级变量的初始化顺序是未定义的,依赖于具体编译器实现。在下面这个Go白皮书中的例子中,

  • 变量a肯定在变量b之后初始化;
  • 但是变量x有可能在变量b之间、或者在变量b和变量a之间、或者在变量a之后初始化,取决于具体的编译器实现;
  • 函数sideEffect()有可能在变量x初始化之前或者之后被调用,取决于具体的编译器实现。
  1. // x是否依赖于a和b,不同的编译器有不同的见解。
  2. var x = I(T{}).ab()
  3. // 假设函数sideEffect和x、a以及b均无关系。
  4. var _ = sideEffect()
  5. var a = b
  6. var b = 42
  7. type I interface { ab() []int }
  8. type T struct{}
  9. func (T) ab() []int { return []int{a, b} }

通常估值顺序(The Usual Order)

Go白皮书这样描述通常估值顺序: …,当估值一个表达式、赋值语句或者函数返回语句中的操作数时,所有的函数调用、方法调用和通道操作将按照它们在代码中的出现顺序进行估值。

注意:一个显式类型转换T(v)不属于函数调用。 举个例子,在表达式[]int{x, fa(), fb(), y}中,假设xy是两个int类型的变量,fafb是两个返回值为int类型的函数,则调用fa()保证在调用fb()之前执行。但是,下面这两个估值顺序没有在Go白皮书中指定:

  • 变量x(或者y)和调用fa()(或者fb())的相对估值顺序;
  • 变量x、变量y、函数值fa和函数值fb的相对估值顺序。下面是Go白皮书中提到的另一个例子:
  1. y[z.f()], ok = g(h(a, b), i()+x[j()], <-c), k()

在此赋值语句中,

  • c是一个通道表达式,它将被估值为一个通道值;
  • ghijk是一些函数表达式,它们将被估值为一些函数值;
  • f是表达式z值的一个方法。综合考虑上一节和本节上面已经提到的规则,编译器应该保证下列在运行时刻的估值顺序:
  • 此赋值中涉及到的函数调用、方法调用和通道操作必须按照这样的顺序执行:z.f()h()i()j()<-cg()k()
  • 调用h()在表达式hab估值之后调用;
  • y[]在方法调用z.f()执行之后被估值;
  • 方法调用z.f()在表达式z估值之后执行;
  • x[]在调用j()执行之后被估值。然而,下列次序在Go白皮书中未指定,它们依赖于具体编译器实现。
  • 表达式yzghabxijck之间的相对估值顺序;
  • 表达式y[]x[]<-c之间的相对估值顺序。根据上述通常估值顺序规则,我们知道下面声明的变量xmn的初始值可能将出现歧义。
  1. a := 1
  2. f := func() int { a++; return a }
  3. // x可能初始化为[1, 2]或者[2, 2],
  4. // 因为a和f()的相对估值顺序未指定。
  5. x := []int{a, f()}
  6. // m可能初始化为{2: 1}或者{2: 2},
  7. // 因为两个映射元素的赋值顺序未指定。
  8. m := map[int]int{a: 1, a: 2}
  9. // n可能初始化为{2: 3}或者{3: 3},
  10. // 因为a和f()的相对估值顺序未指定。
  11. n := map[int]int{a: f()}

赋值语句中的表达式估值和赋值执行顺序

除了上面介绍的规则,Go白皮书对赋值语句中的表达式估值和各个单值赋值执行顺序进行了更多描述(原英文描述不是十分精确,在翻译过程中对之略加改进): 一条赋值语句的执行分为两个阶段。首先,做为目标值的元素索引表达式中的容器值表达式和索引值表达式、做为目标值的指针解引用表达式中的指针值表达式、以及此赋值语句中的其它非目标值表达式将按照上述通常估值顺序估值。然后,各个单值赋值将按照从左到右的顺序执行。

以后,我们可以称第一个阶段为估值阶段,称第二个阶段为实施阶段。

Go白皮书并没有清楚地说明在第二个阶段中发生的赋值操作是否会对在第一个阶段结尾确定下来的各个子表达式的估值结果造成影响,此举曾造成了一些争议。所以,这里下面将对赋值语句中的表达式估值顺序做出一些补充解释。

首先,先明确一下:第二个阶段中发生的赋值操作绝不会对在第一个阶段结尾确定下来的各个子表达式的估值结果造成影响.

为了方便下面的解释,对于一个赋值语句,我们假设一个做为目标值的容器(切片或者映射)元素索引表达式中的映射值总是可寻址的。如果它是不可寻址的,我们可以认为在实施第二个阶段之前,此容器值已经被赋给了一个临时变量(可寻址的)并且在此赋值语句中此容器值已经被此临时变量取代。 在估值阶段结束之后、实施阶段开始之前的时刻,赋值语句中的每个目标值表达式都已经被估值为它的最基本形式。不同风格的目标值表达式有着不同的最基本形式:

  • 如果一个目标值表达式是一个空标识符,则它的最基本形式依旧是一个空标识符;
  • 如果一个目标值表达式是一个容器(数组或者切片或者映射)元素索引表达式c[k],则它的最基本形式为(*cAddr)[k],其中一个cAddr为指向c的指针;
  • 对于其它情形的任何一个目标值表达式,它必然是可寻址的,则它的最基本形式为它的地址的解引用形式。假设ab为两个可寻址的同类型变量,则下面的赋值语句
  1. a, b = b, a

将按照如下步骤执行:

  1. // 估值阶段
  2. P0 := &a; P1 := &b
  3. R0 := a; R1 := b
  4. // 最基本形式:*P0, *P1 = R0, R1
  5. // 实施阶段
  6. *P0 = R0
  7. *P1 = R1

下面是另外一个例子,其中x[0]而不是x[1]被修改了。

  1. x := []int{0, 0}
  2. i := 0
  3. i, x[i] = 1, 2
  4. fmt.Println(x) // [2 0]

上例中的第3行的分解执行步骤如下:

  1. // 估值阶段
  2. P0 := &i; P1 := &x; T2 := i
  3. R0 := 1; R1 := 2
  4. // 到这里,T2 == 0
  5. // 最基本形式:*P0, (*P1)[T2] = R0, R1
  6. // 实施阶段
  7. *P0 = R0
  8. (*P1)[T2] = R1

下面是一个略为复杂一点的例子。

  1. package main
  2. import "fmt"
  3. func main() {
  4. m := map[string]int{"Go": 0}
  5. s := []int{1, 1, 1}; olds := s
  6. n := 2
  7. p := &n
  8. s, m["Go"], *p, s[n] = []int{2, 2, 2}, s[1], m["Go"], 5
  9. fmt.Println(m, s, n) // map[Go:1] [2 2 2] 0
  10. fmt.Println(olds) // [1 1 5]
  11. }

上例中的第10行的分解执行步骤如下:

  1. // 估值阶段
  2. P0 := &s; PM1 := &m; K1 := "Go"; P2 := p; PS3 := &s; T3 := 2
  3. R0 := []int{2, 2, 2}; R1 := s[1]; R2 := m["Go"]; R3 := 5
  4. // 到这里,R1 == 1, R2 == 0
  5. // 最基本形式:*P0, (*PM1)[K1], *P2, (*PS3)[T3] = R0, R1, R2, R3
  6. // 实施阶段
  7. *P0 = R0
  8. (*PM1)[K1] = R1
  9. *P2 = R2
  10. (*PS3)[T3] = R3

下面这个例子将一个切片中的所有元素循环顺移了一位。

  1. x := []int{2, 3, 5, 7, 11}
  2. t := x[0]
  3. var i int
  4. for i, x[i] = range x {}
  5. x[i] = t
  6. fmt.Println(x) // [3 5 7 11 2]

另一个例子:

  1. x := []int{123}
  2. x, x[0] = nil, 456 // 此句不会发生恐慌
  3. x, x[0] = []int{123}, 789 // 此句将产生恐慌

尽管使用复杂的多值赋值语句是合法的,但是在实践中并不推荐使用,因为复杂多值赋值语句的可读性不高,编译速度较慢,并且执行效率相对略低。 上面已经提到了,并非所有的估值顺序都在Go白皮书中指定清楚了,所以不同的Go编译器对这些未指定的估值顺序有着自己的理解。一些跨编译器兼容性不好的代码将导致使用不同的编译器编译的程序的运行结果不同。在下面这个例子中,表达式x+1f(&x)的估值顺序是编译器相关的,所以此程序输出100 99或者1 99都是合理的。

  1. package main
  2. import "fmt"
  3. func main() {
  4. f := func (p *int) int {
  5. *p = 99
  6. return *p
  7. }
  8. x := 0
  9. y, z := x+1, f(&x)
  10. fmt.Println(y, z)
  11. }

下面是另一个可能输出不同结果的程序。它可能输出1 7 21 8 2或者1 9 2,取决于不同的编译器实现。

  1. package main
  2. import "fmt"
  3. func main() {
  4. x, y := 0, 7
  5. f := func() int {
  6. x++
  7. y++
  8. return x
  9. }
  10. fmt.Println(f(), y, f())
  11. }

switch-case流程控制代码块中的表达式估值顺序

switch-case流程控制代码块中的表达式估值顺序已经在前面的文章中大致描述过了。这里仅仅展示一个例子。简单来说,在进入一个分支代码块之前,各个case关键字后跟随的表达式将按照从上到下和从左到右的顺序进行估值,直到某个比较结果为true为止。

  1. package main
  2. import "fmt"
  3. func main() {
  4. f := func(n int) int {
  5. fmt.Printf("f(%v) is called.\n", n)
  6. return n
  7. }
  8. switch x := f(3); x + f(4) {
  9. default:
  10. case f(5):
  11. case f(6), f(7), f(8):
  12. case f(9), f(10):
  13. }
  14. }

在运行时刻,各个f()调用将按照传给它们参数的大小顺序进行估值,直到和调用f(7)的比较结果为true为止。所以调用f(8)f(9)f(10)将不会被估值。 输出结果:

  1. f(3) is called.
  2. f(4) is called.
  3. f(5) is called.
  4. f(6) is called.
  5. f(7) is called.

select-case流程控制代码块中的表达式估值顺序

当执行一个select-case流程控制代码块时,各个case关键值后跟随的所有通道操作中的通道表达式和所有通道发送操作中的发送值表达式都将被按照它们在代码中的出现次序(从上到下从左到右)估值一次。

注意:以通道接收操作做为源值的赋值语句中的目标值表达式只有在此通道接收操作被选中之后才会被估值。 比如,在下面这个例子中,表达式*fptr("aaa")将永不会得到估值,因为它对应的通道接收操作<-fchan("bbb", nil)是个不可能被选中的阻塞操作。

  1. package main
  2. import "fmt"
  3. func main() {
  4. c := make(chan int, 1)
  5. c <- 0
  6. fchan := func(info string, c chan int) chan int {
  7. fmt.Println(info)
  8. return c
  9. }
  10. fptr := func(info string) *int {
  11. fmt.Println(info)
  12. return new(int)
  13. }
  14. select {
  15. case *fptr("aaa") = <-fchan("bbb", nil): // blocking
  16. case *fptr("ccc") = <-fchan("ddd", c): // non-blocking
  17. case fchan("eee", nil) <- *fptr("fff"): // blocking
  18. case fchan("ggg", nil) <- *fptr("hhh"): // blocking
  19. }
  20. }

上例的输出结果:

  1. bbb
  2. ddd
  3. eee
  4. fff
  5. ggg
  6. hhh
  7. ccc

注意,表达式*fptr("ccc")是上例中最后一个被估值的表达式。它在对应的数据接收操作<-fchan("ddd", c)被选中之后才会进行估值。

Go语言101项目目前同时托管在GithubGitlab上。欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏