Go 语言
声明、类型、语句与控制结构
表达式求值顺序

表达式求值顺序

Go 支持在同一行声明和初始化多个变量(不同类型也可以)

var a, b, c int = 1, 2, 3
var d, e = 123, "hello"

Go 支持在同一行对多个变量进行赋值

a, b, c = 1, "hello", 3

变量声明语句求值

在一个 Go 包内部,包级别的变量声明的表达式的求值顺序是由初始化依赖的规则决定的。规则总结如下:

  • 包级别变量的初始化按照变量声明的先后顺序进行。
  • 如果某个变量a的初始化表达式中直接或间接依赖其他变量b,那么变量a的初始化顺序排在变量b的后面。
  • 未初始化的且不含有对应初始化表达式或初始化表达式不依赖任何未初始化变量的变量,我们称为 “ready for initialization" 变量.
  • 包级别的变量的初始化是逐步进行的,每一步就是按照变量声明顺序找到下一个 “ready for initialization" 变量进行初始化。反复这个过程直至没有了 “ready for initialization" 变量。
  • 位于同一包内但不同文件中的变量声明顺序依赖编译器处理文件的顺序:先处理的文件中的变量顺序先于后处理的文件中的所有变量。
var (
    a = c + b
    b = f()
    c = f()
    d = 3
)
 
func f() int {
    d++
    return d
}
 
func main() {
    println(a, b, c, d) // 9 4 5 5
}

如果在包级别变量声明中使用了空变量_,空变量也会得到 Go 编译器一视同仁的对待。

普通求值顺序

Go 还定义了普通求值顺序,用于规则表达式操作数中的函数,方法及 channel 操作的求值顺序。表达式操作数的求值顺序是从左到右的。

func f() int {
    fmt.Println("f called")
    return 1
}
 
func g() int {
    fmt.Println("g called")
    return 2
}
 
func main() {
    fmt.Println(f() + g())
}
 
// f called
// g called
// 3

赋值语句的求值

n0, n1 = n0 + n1, n0
// 或
n0, n1 = op(n0, n1), n0

这是一个赋值语句,赋值语句的求值分为两个阶段:

  • 第一阶段:对于等号左边的下标表达式、指针解引用表达式和等号右边表达式中的操作数,按照普通求值规则从左到右依次进行求值。
  • 第二阶段:按照从左到右的顺序对变量进行赋值。
func main() {
    n0, n1 := 1, 2
    n0, n1 = n0 + n1, n0
    fmt.Println(n0, n1) // 3 1
}

switch/select 的求值

switch-case

我们先看 switch-case 语句的表达式求值,这类求值属于”惰性求值“范畴,惰性求值是指在需求进行求职时才会对表达式进行求职,这样做的目的是让计算机少做事情,提高程序的性能。

func Expr(n int) int {
    fmt.Println(n)
    return n
}
 
func main() {
    switch Expr(2) {
    case Expr(1), Expr(2), Expr(3):
        fmt.Println("first case")
        fallthrough
    case Expr(4):
        fmt.Println("second case")
    }
}
 
// 2
// 1
// 2
// first case
// second case"

上面的例子中,最先求值的是 switch 后面的 Expr(2),所以输出 2; 然后从上到下,从做到右,对 case 语句里的表达式进行求职; 如果某个表达式的值和 switch 后面表达式的值相等,那么求值停止,后面的未求值的表达式就会被忽略,所以Expr(3)就没执行; fallthrough 将执行权直接转移到下一个 case 语句,所以 Expr(4) 也没执行。

select-case

Go 语言中的 select 为我们提供了一种在多个 channel 间实现多路复用的能力,是编写 Go 并发程序最常用的并发原语之一。

func getAReadOnlyChannel() <-chan int {
    println("do getAReadOnlyChannel")
    ch := make(chan int, 1)
    go func() {
        time.Sleep(3*time.Second)
        ch <- 1
    }()
    return ch
}
 
func getASlice() *[5]int {
    println("do getASlice")
    var a [5]int
    return &a
}
 
func getAWriteOnlyChannel() chan<- int {
    println("do getAWriteOnlyChannel")
    return make(chan int, 1)
}
 
func getNumToChannel() int {
    println("do getNumToChannel")
    return 2
}
 
func main() {
    select {
    case (getASlice())[0] = <-getAReadOnlyChannel():
        println("case 1")
    case getAWriteOnlyChannel() <- getNumToChannel():
        println("case 2")
    }
}
 
// do getAReadOnlyChannel
// do getAWriteOnlyChannel
// do getNumToChannel
// do getASlice
// case 1

从上面代码可以看到两点:

  1. select 语句中的 case 语句的求值顺序是从上到下、从左到右的,按照出现的顺序求值一遍。
  2. 如果选择要执行一个从 channel 接收数据的 case ,那么该 case 等号左边的表达式在接收前才会被求值。这也算是一种惰性求值。