Go基础12-理解Go语言表达式的求值顺序

这篇具有很好参考价值的文章主要介绍了Go基础12-理解Go语言表达式的求值顺序。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Go语言在变量声明、初始化以及赋值语句上相比其先祖C语言做了一些改进,诸如:

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

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

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

a, b, c = 5, "hello", 3.45

这种语法糖在给我们带来便利的同时,也可能带来一些令人困惑的问题。

Go语言之父Rob Pike在Go语言早期(r60版本,2011年)曾经讲过一门名为“The GoProgramming Language” [1] 的课程,虽然距今年代有些久远,但该课程仍然是笔者心中的经典,强烈推荐Gopher学习一下。

在该门课程第二天 [2] 的内容中,Rob Pike出了这样一道练习题:下面语句执行完毕后,n0和n1的值分别是多少?

n0, n1 = n0+n1, n0

或者

n0, n1 = op(n0, n1), n0

对于这个问题,很多Go语言初学者无法给出答案;一些Go语言老手虽然能给出正确答案,但也说不出个所以然。显然这个问题涉及Go语言的表达式求值顺序(evaluationorder)。

上面问题中赋值语句中的表达式求值仅仅是表达式求值的众多应用场景中的一个。表达式的求值顺序在任何一门编程语言中都是比较“难缠的”。很多情形下,语言规范给出的 答 案 可 能 是“undefined”(未 定 义)、“not specified”(未 明 确 说 明)或“implementation-dependent”(实现相关)。

理解表达式求值顺序的机制,对于编写出正确、逻辑清晰的Go代码很有必要,因此在这一条中,我们一起结合直观的实例来深入理解Go语言的表达式求值顺序。

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

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

● 在Go包中,包级别变量的初始化按照变量声明的先后顺序进行。

● 如果某个变量(如变量a)的初始化表达式中直接或间接依赖其他变量(如变量b),那么变量a的初始化顺序排在变量b后面。

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

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

● 位于同一包内但不同文件中的变量的声明顺序依赖编译器处理文件的顺序:先处理的文件中的变量的声明顺序先于后处理的文件中的所有变量。规则往往抽象难懂,例子则更直观易理解。

我们看一个Go语言规范中的例子,并使用
上述规则进行分析(Go编译器版本1.13):

package main

import "fmt"

var (
	a = c + b
	b = f()
	c = f()
	d = 3
)

func f() int {
	d++
	return d
}
func main() {
	fmt.Println(a, b, c, d)
}

运行结果:

9 4 5 5

对于上面的代码,不同的包变量初始化顺序会导致变量值不同,因此明确四个变量的初始化顺序至关重要。我们结合上面的初始化依赖规则来分析一下该程序执行后的a、b、c、d四个变量的结果值。

1)根据规则,包级变量初始化按照变量声明先后顺序进行,因此每一轮寻找“readyfor initialization”变量的过程都会按照a -> b -> c -> d的顺序依次进行。

2)我们先来进行第一轮选择“ready for initialization”变量的过程。我们从变量a开始。变量a的初始化表达式为c + b,这使得a的初始化依赖b和c,而b、c通过函数f间接依赖未初始化变量d,因此a并不是“ready for initialization”变量。

3)按照声明顺序,接下来是b。b的初始化表达式依赖函数f,而函数f依赖未初始化变量d,因此b也不是“ready for initialization”变量。

4)按照声明顺序,接下来是c。c的初始化表达式依赖函数f,而函数f依赖未初始化变量d,因此c也不是“ready for initialization”变量。

5)按照声明顺序,接下来是d。d没有需要求值的初始化表达式,而是直接被赋予了初值,因此d是我们第一轮找到的“ready for initialization”变量,我们对其进行初始化:d = 3。当前已初始化变量集合为[d=3]。

6)接下来进行第二轮“ready for initialization”变量的寻找。我们依然从a开始,和第一轮一样,b、c依旧是未初始化变量,a不符合条件;我们继续看b。b依赖函数f,函数f依赖d,但d已经是已初始化变量集合中的元素了,因此b具备了成为“ready forinitialization”的条件,于是第二轮我们选出了b,并对b进行初始化:b = d + 1 = 4。
此时已初始化变量集合为[d=4, b=4]。

7)接下来进行第三轮“ready for initialization”变量的寻找。我们依然从a开始,和前两轮一样,c依旧是未初始化变量,a不符合条件;我们继续看c。c依赖函数f,函数f依赖d,但d已经是已初始化变量集合中的元素了,因此c具备了成为“ready forinitialization”的条件,于是第三轮我们选出了c,并对c进行初始化:c = d + 1 = 5。
此时已初始化变量集合为[d=5, b=4, c=5]。

8)接下来进行最后一轮“ready for initialization”变量的寻找。此时只剩下变量a了,并且a依赖的b、c都是已初始变量集合中的元素了,因此a符合“ready forinitialization”的条件,于是最后一轮我们选出a,并对a进行初始化:a = 4 + 5 = 9。
此时已初始化变量集合为[d = 5, b = 4, c = 5, a = 9]。
9)初始化结束,根据上述分析,程序应该输出9 4 5 5。

如果在包级变量声明中使用了空变量_,空变量也会得到Go编译器一视同仁的对待。我们看下面的例子:

package main

import "fmt"

var (
	a = c + b
	b = f()
	_ = f()
	c = f()
	d = 3
)

func f() int {

	d++
	return d
}
func main() {

	fmt.Println(a,b,c,d)
}

有了第一个例子中详细的分析,这里我们的分析从简。

1) 初 始 化 过 程 按 照 a - > b - > _ - > c - > d 的 顺 序 进 行“ready forinitialization”变量的查找。

2)第一轮:变量a、b、_、c都不符合条件,d被选出并初始化,已初始化变量集合为[d=3]。
3)第二轮:变量b符合条件被选出并初始化,已初始化变量集合为[d=4, b=4]

4)第三轮:空变量符合条件被选出并初始化,但空变量忽略了初始值,这一过程的副作用是使得变量d增加1,已初始化变量集合为[d=5, b=4]。

5)第四轮:变量c符合条件被选出并初始化,已初始化变量集合为[d=6, b=4, c=6]。

6)第五轮:变量a符合条件被选出并初始化,已初始化变量集合为[d=6, b=4, c=6,a=10]。

7)包变量初始化结束,分析输出结果应为10 4 6 6。


还有一种比较特殊的情况值得我们在这里一并分析,那就是当多个变量在声明语句左侧且右侧为单一表达式时的表达式求值情况。在这种情况下,无论左侧哪个变量被初始化,同一行的其他变量也会被一并初始化。

我们来看下面这个例子:

package main

import "fmt"

var (
	a    = c
	b, c = f()
	d    = 3
)

func f() (int, int) {

	d++
	return d, d + 1
}
func main() {

	fmt.Println(a, b, c, d)
}

1)根据包级变量初始化规则,初始化过程将按照a -> b&c -> d顺序进行“ready forinitialization”变量的查找。

2)第一轮:变量a、b、c都不符合条件,d被选出并初始化,已初始化变量集合为[d=3]。

3)第二轮:变量b和c一起符合条件,以b被选出为例,b被初始化的同时,c也得到了
初始化,因此已初始化变量集合为[d=4, b=4, c=5]。

4)第三轮:变量a符合条件被选出并初始化,已初始化变量集合为[d=4, b=4, c=5,a=5]。

5)包变量初始化结束,分析输出结果应为5 4 5 4。
运行上述代码:

5 4 5 4

输出结果也与我们分析的一致。

普通求值顺序

除了包级变量由初始化依赖决定的求值顺序,Go还定义了普通求值顺序(usualorder),用于规定表达式操作数中的函数、方法及channel操作的求值顺序。Go规定表达式操作数中的所有函数、方法以及channel操作按照从左到右的次序进行求值。

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

package main

import "fmt"

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 = []int{11, 12, 13}
	var x = []int{21, 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操作按从左到右的顺序进行求值。

● 按照从左到右的顺序,先对等号左侧表达式操作数中的函数进行调用求值,因此第一个是y[f()]中的f()。

● 接下来是等号右侧的表达式。第一个函数是g(),但g()依赖其参数的求值,其参数列表依然可以看成是一个多值赋值操作,其涉及的函数调用顺序从左到右依次为h()、i()、j()、<-c,这样该表达式操作数函数的求值顺序即为h() -> i() -> j() -> c取值操作 ->g()。

● 最后还剩下末尾的k(),因此该语句中函数以及channel操作的完整求值顺序是:f() ->h() -> i() -> j() -> c取值操作 -> g() -> k()。
例子的实际运行结果如下:

calling f
calling h
calling i
calling j
calling g
calling k

输出结果与我们分析的一致。

赋值语句的求值

package main

import "fmt"

func example() {
	n0, n1 := 1, 2
	n0, n1 = n0+n1, n0
	fmt.Println(n0, n1)
}
func main() {
	example()
}

这是一个赋值语句。Go语言规定,赋值语句求值分为两个阶段:、

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

2)第二阶段,按从左到右的顺序对变量进行赋值。
根据上述规则,我们对这个问题等号两端的表达式的操作数采用从左到右的求值顺序。

假定n0和n1的初值如下:n0, n1 = 1, 2
第一阶段:等号两端表达式求值。上述问题中,等号左边没有需要求值的下标表达式、指针解引用表达式等,只有右端有n0+n1和n0两个表达式,但表达式的操作数(n0,n1)
都是已初始化了的,因此直接将值代入,得到求值结果。

求值后,语句可以看成n0, n1 =3, 1。

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


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

上面的三类求值顺序原则已经可以覆盖大部分Go代码中的场景了,如果说在表达式求值方面还有值得重点关注的,那肯定非switch/select语句中的表达式求值莫属了。

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

package main

import "fmt"

func Expr(n int) int {
	fmt.Println(n)
	return n
}
func main() {
	switch Expr(2) {
	case Expr(1), Expr(2), Expr(3):
		fmt.Println("enter into case1")
		fallthrough
	case Expr(4):
		fmt.Println("enter into case2")
	}
}

运行结果:

2
1
2
enter into case1
enter into case2

从例子的输出结果我们看到:

1)对于switch-case语句而言,首先进行求值的是switch后面的表达式Expr(2),这个表达式在求值时输出2。

2)接下来将按照从上到下、从左到右的顺序对case语句中的表达式进行求值。如果某个表达式的结果与switch表达式结果一致,那么求值停止,后面未求值的case表达式将被
忽略。结合上述例子,这里对第一个case中的Expr(1)和Expr(2)进行了求值,由于Expr(2)
求值结果与switch表达式的一致,所以后续Expr(3)并未进行求值。

3)fallthrough将执行权直接转移到下一个case执行语句中了,略过了case表达式Expr(4)的求值。


我们再来看看select-case语句的求值。Go语言中的select为我们提供了一种在多个channel间实现“多路复用”的机制,是编写Go并发程序最常用的并发原语之一。

我们通过一个例子直观看一下select-case语句中表达式的求值规则:

package main

import (
	"fmt"
	"time"
)

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")
	}
}

运行结果:

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())[0]进行求值,输出“invoke getASlice”,这也算是一种惰性求
值。


表达式本质上就是一个值,表达式求值顺序影响着程序的计算结果。
Gopher应牢记以下几点规则。

● 包级别变量声明语句中的表达式求值顺序由变量的声明顺序和初始化依赖关系决定,
并且包级变量表达式求值顺序优先级最高。

● 表达式操作数中的函数、方法及channel操作按普通求值顺序,即从左到右的次序进
行求值。

● 赋值语句求值分为两个阶段:先按照普通求值规则对等号左边的下标表达式、指针解
引用表达式和等号右边的表达式中的操作数进行求值,然后按从左到右的顺序对变量
进行赋值。

● 重点关注switch-case和select-case语句中的表达式“惰性求值”规则。文章来源地址https://www.toymoban.com/news/detail-703309.html

到了这里,关于Go基础12-理解Go语言表达式的求值顺序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Go】Go 文本匹配 - 正则表达式基础与编程中的应用 (8000+字)

             本文共计8361字,预计阅读时间18分钟         正则表达式(Regular Expression, 缩写常用regex, regexp表示)是计算机科学中的一个概念,很多高级语言都支持正则表达式。 目录 何为正则表达式 语法规则 普通字符 字符转义 限定符 定位符 分组构造 模式匹配 regexp包

    2024年02月12日
    浏览(39)
  • 超详解 - 如何理解C语言中while(scanf(“%d“, &num) != EOF)这一表达式?

    许多C语言初学者常常对scanf函数、表达式scanf(\\\"%d\\\", num) != EOF的含义与其使用情况有些疑惑。 本文通过一道牛客网例题,对该表达式进行说明和适当拓展;不需要引例的朋友可以直接跳转到讲解部分。 希望对诸位读者有所帮助。 目录 一、引例 - 牛客网OJ题 二、EOF 与 scanf 函数

    2024年02月08日
    浏览(39)
  • LangChain 67 深入理解LangChain 表达式语言30 调用tools搜索引擎 LangChain Expression Language (LCEL)

    LangChain系列文章 LangChain 50 深入理解LangChain 表达式语言十三 自定义pipeline函数 LangChain Expression Language (LCEL) LangChain 51 深入理解LangChain 表达式语言十四 自动修复配置RunnableConfig LangChain Expression Language (LCEL) LangChain 52 深入理解LangChain 表达式语言十五 Bind runtime args绑定运行时参数

    2024年01月23日
    浏览(73)
  • 【数据结构与算法】【12】前缀表达式、中缀表达式、后缀表达式

    什么是前缀表达式、中缀表达式、后缀表达式 前缀表达式、中缀表达式、后缀表达式,是通过树来存储和计算表达式的三种不同方式 以如下公式为例 ( a + ( b − c ) ) ∗ d ( a+(b-c) )*d ( a + ( b − c ) ) ∗ d 通过树来存储该公式,可以表示为 那么问题就来了,树只是一种抽象的数据

    2024年02月08日
    浏览(44)
  • 初始Go语言2【标识符与关键字,操作符与表达式,变量、常量、字面量,变量作用域,注释与godoc】

      go变量、常量、自定义类型、包、函数的命名方式必须遵循以下规则: 首字符可以是任意Unicode字符或下划线。 首字符之外的部分可以是Unicode字符、下划线或数字。 名字的长度无限制。 理论上名字里可以有汉字,甚至可以全是汉字,但实际中不要这么做。 Go语言

    2023年04月09日
    浏览(52)
  • go 正则表达式

    A regular expression is a useful feature in a programming language to check whether or not the string contains the desired value. It can not only check but also extract the data from the string. In this post, we’ll go through the basic usage of regexp . Let’s start with an easy example. The first one only checks if the value is contained in a string. reg

    2024年02月11日
    浏览(39)
  • 软考:中级软件设计师:程序语言基础:表达式,标准分类,法律法规,程序语言特点,函数传值传址

    提示:系列被面试官问的问题,我自己当时不会,所以下来自己复盘一下,认真学习和总结,以应对未来更多的可能性 关于互联网大厂的笔试面试,都是需要细心准备的 (1)自己的科研经历, 科研内容 ,学习的相关领域知识,要熟悉熟透了 (2)自己的实习经历,做了 什

    2024年02月09日
    浏览(58)
  • 12.字符串和正则表达式

    正则表达式相关知识 在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要,正则表达式就是用于描述这些规则的工具,换句话说正则表达式是一种工具,它定义了字符串的匹配模式(如何检查一个字符串是否有跟某种模式匹配的部分或者从一个

    2024年01月16日
    浏览(56)
  • GO学习笔记之表达式

    Go语言仅25个保留(keyword),这是最常见的宣传语,虽不是主流语言中最少的,但也确实体现了Go语法规则的简洁性。保留不能用作常量、变量、函数名,以及结构字段等标识符。 相比在更新版本中不停添加新语言功能,我更喜欢简单的语言设计。某些功能可通

    2024年02月08日
    浏览(31)
  • 12 正则表达式 | HTTP协议相关介绍

    在 Python 中需要通过正则表达式对字符串进行匹配的时候,可以使用一个模块,名字为 re。 示例: 输出的结果: 说明:re.match() 能够匹配出以== xxx 开头==的字符串 字符 功能 . 匹配任意 1 个字符(除了n) [ ] 匹配[ ]中列举的字符 d 匹配数字,即 0-9 dicimal D 匹配非数字,即不

    2024年02月12日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包