《Go语言精进之路》读书笔记 | 理解Go语言表达式的求值顺序

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 理解Go语言表达式的求值顺序相关的知识,希望对你有一定的参考价值。

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》

一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客


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

var a, b, c = 5, "hello", 3.45
a, b, c := 5, "hello", 3.45 // 短变量声明形式
复制代码

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

a, b, c = 5, "hello", 3.45
复制代码

这种语法糖在给我们带来便利的同时,也可能带来一些令人困惑的问题。表达式的求值顺序在任何一门编程语言中都是比较“难缠的”。

理解表达式求值顺序的机制,对于编写出正确、逻辑清晰的Go代码很有必要。

17.1 包级别变量声明语句中的表达式求值顺序

包级别变量声明语句的表达式求值顺序是由初始化依赖规则决定的。那初始化依赖规则是什么呢?根据Go语言规范中的说明,这里将该规则总结为如下几点。

在Go包中,包级别变量的初始化按照变量声明的先后顺序进行。如果某个变量(如变量a)的初始化表达式中直接或间接依赖其他变量(如变量b),那么变量a的初始化顺序排在变量b后面。

未初始化的且不含有对应初始化表达式或初始化表达式不依赖任何未初始化变量的变量,我们称之为“ready for initialization”变量。

包级别变量的初始化是逐步进行的,每一步就是按照变量声明顺序找到下一个“ready for initialization”变量并对其进行初始化的过程。反复重复这一步骤,直到没有“ready for initialization”变量为止。

位于同一包内但不同文件中的变量的声明顺序依赖编译器处理文件的顺序:先处理的文件中的变量的声明顺序先于后处理的文件中的所有变量。

17.2 普通求值顺序

Go还定义了普通求值顺序(usual order),用于规定表达式操作数中的函数、方法及channel操作的求值顺序。Go规定表达式操作数中的所有函数、方法以及channel操作按照从左到右的次序进行求值。

同样来看一个改编自Go语言规范中的例子:

// chapter3/sources/evaluation_order_4.go
func f() int 
    fmt.Println("calling f")
    return 1


func g(a, b, c int) int 
    fmt.Println("calling g")
    return 2


func h() int 
    fmt.Println("calling h")
    return 3


func i() int 
    fmt.Println("calling i")
    return 1


func j() int 
    fmt.Println("calling j")
    return 1


func k() bool 
    fmt.Println("calling k")
    return true


func main() 
    var y = []int11, 12, 13
    var x = []int21, 22, 23

    var c chan int = make(chan int)
    go func() 
        c <- 1
    ()

    y[f()], _ = g(h(), i()+x[j()], <-c), k()

复制代码

y[f()], _ = g(h(), i()+x[j()], <-c), k()这行语句是赋值语句,但赋值语句的表达式操作数中包含函数调用、channel操作。按照普通求值规则,这些函数调用、channel操作按从左到右的顺序进行求值。

该语句中函数以及channel操作的完整求值顺序是:f()-> h() -> i() -> j() -> c取值操作 -> g() -> k()。

当普通求值顺序与包级变量的初始化依赖顺序一并使用时,后者优先级更高,但每个单独表达式中的操作数求值依旧按照普通求值顺序的规则。

17.3 赋值语句的求值

Rob Pike出过这样一道练习题:下面语句执行完毕后,n0和n1的值分别是多少?

n0, n1 = n0 + n1, n0

// 或者

n0, n1 = op(n0, n1), n0
复制代码

这个赋值语句求值分为两个阶段:

1)第一阶段,对于等号左边的下标表达式、指针解引用表达式和等号右边表达式中的操作数,按照普通求值规则从左到右进行求值;

2)第二阶段,按从左到右的顺序对变量进行赋值。

假定n0和n1的初值如下:

n0, n1 = 1, 2
复制代码

第一阶段:等号两端表达式求值。等号左边没有需要求值的,只有右端有n0+n1和n0两个表达式,(n0,n1)已初始化,直接将值代入,求值后,语句可以看成n0, n1 = 3, 1。

第二阶段:从左到右赋值,即n0 =3,n1 = 1。

17.4 switch/select语句中的表达式求值

如果说在表达式求值方面还有值得重点关注的,那肯定非switch/select语句中的表达式求值莫属了。

switch-case语句中的表达式求值,属于“惰性求值”范畴。惰性求值指的就是需要进行求值时才会对表达值进行求值,这样做的目的是让计算机少做事,从而降低程序的消耗,对性能提升有一定帮助。

Go语言中的select为我们提供了一种在多个channel间实现“多路复用”的机制,是编写Go并发程序最常用的并发原语之一。通过一个例子直观看一下select-case语句中表达式的求值规则:

// chapter3/sources/evaluation_order_8.go

func getAReadOnlyChannel() <-chan int 
    fmt.Println("invoke getAReadOnlyChannel")
    c := make(chan int)

    go func() 
        time.Sleep(3 * time.Second)
        c <- 1
    ()

    return c


func getASlice() *[5]int 
    fmt.Println("invoke getASlice")
    var a [5]int
    return &a


func getAWriteOnlyChannel() chan<- int 
    fmt.Println("invoke getAWriteOnlyChannel")
    return make(chan int)


func getANumToChannel() int 
    fmt.Println("invoke getANumToChannel")
    return 2


func main() 
    select 
    // 从channel接收数据
    case (getASlice())[0] = <-getAReadOnlyChannel():
       fmt.Println("recv something from a readonly channel")
    // 将数据发送到channel
    case getAWriteOnlyChannel() <- getANumToChannel():
        fmt.Println("send something to a writeonly channel")
    

复制代码

该程序的运行结果如下:

$go run evaluation_order_8.go
invoke getAReadOnlyChannel
invoke getAWriteOnlyChannel
invoke getANumToChannel

invoke getASlice
recv something from a readonly channel
复制代码

从上述例子可以看出以下两点。

1)select执行开始时,首先所有case表达式都会被按出现的先后顺序求值一遍。

invoke getAReadOnlyChannel
invoke getAWriteOnlyChannel
invoke getANumToChannel
复制代码

有一个例外,位于case等号左边的从channel接收数据的表达式(RecvStmt)不会被求值,这里对应的是getASlice()。

2)如果要执行的是一个从channel接收数据的case,case等号左边的表达式在接收前才会被求值。上面例子中,getAReadOnlyChannel创建的goroutine在3s后向channel中写入一个int值,之后select选择了第一个case执行,对getASlice()进行求值,输出“invoke getASlice”,这也算是一种惰性求值。

以上是关于《Go语言精进之路》读书笔记 | 理解Go语言表达式的求值顺序的主要内容,如果未能解决你的问题,请参考以下文章

《Go语言精进之路》读书笔记 | 理解Go语言的包导入

《Go语言精进之路》读书笔记 | 理解Go语言代码块与作用域

《Go语言精进之路》读书笔记 | 汇总

《Go语言精进之路》读书笔记 | 选择适当的Go语言版本

《Go语言精进之路》读书笔记 | 了解Go语言的诞生与演进

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码