Go语言表达式的求值顺序

 更新时间:2026年06月05日 09:38:45   作者:念何架构之路  
本文详细讲解了Go语言中的变量求值顺序规则,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

1.包级别变量声明语句表达式求值顺序:

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

1).在Go包中.包级别变量的初始化按照变量声明的先后顺序进行.

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

3).未初始化的且不含有对应初始化表达式或初始化表达式不依赖任何未初始化变量的变量.称之为ready for initialization变量.

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

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

示例:

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

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

1).根据规则.包集变量初始化按照变量声明先后顺序进行.因此每一轮寻找"ready for 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.

6).接下来第二轮寻找.依然从a开始.和第一轮一样.b c依旧是未初始化变量.a不满足条件.b依赖函数f.函数f依赖d.但d已经是初始化变量集合中的元素了.b具备了成为"ready for initialization"的条件.于是第二轮对b进行了初始化.b=3+1=4.

7).第三轮寻找.和前两轮一样.c依旧是未初始化变量.a不符合条件.c依赖函数f.函数f依赖d.d已经是已初始化变量集合中的元素了.c具备了条件.对c进行初始化c=4+1=5.

8).进行最后一轮寻找.b和c都已经初始化完成了.所以a=5+4=9.

9).初始化结束.根据上述分析.程序应该输出 9 4 5 5.

执行结果:

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

示例:

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 for initialization"变量查找.

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.

执行结果:

多个变量在声明语句左侧且右侧为单一表达式时的求值情况.这种情况下.无论左侧哪个变量被初始化.同一行的其他变量也会被一并初始化.

示例:

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

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

1).根据包级变量的初始化规则.初始化过程a->b&c->d顺序进行"ready for initialization"变量查找.

2).第一轮:变量 a b c都不符合条件.d被选出初始化.d=3.

3).第二轮.变量b和c一起符合条件.以b被选出为例.b的初始化同时.c也得到初始化.b=3,c=4,d=4.

4).第三轮:变量a符合条件被选出并初始化.d=4,b=3,c=4,a=3.

5).包变量初始化结束.分析输出结果为4 3 4 3.

执行结果:

2.普通求值顺序:

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

示例:

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

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

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

3).最后还剩下末尾的k(),因此该语句中函数以及channel操作的完整求值顺序是.f()->h()->i()->j()->c取值操作->g()->k().

执行结果:

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

示例:

var a, b, c = f() + v(), g(), sqr(u()) + v()

func f() int {
    fmt.Println("calling f")
    return c
}

func g() int {
    fmt.Println("calling g")
    return 1
}

func sqr(x int) int {
    fmt.Println("calling sqr")
    return x * x
}

func v() int {
    fmt.Println("calling v")
    return 1
}

func u() int {
    fmt.Println("calling u")
    return 2
}

func main() {
    fmt.Println(a, b, c)
}

根据包变量初始化依赖规则以及普通求值顺序规则对这个例子进行简要分析.把单行的声明语句等价转换为下面的代码.这样看起来更直观.(注意:与前面多个变量在声明语句左侧且右侧为单一表达式时的表达式求值情况不同.这里右侧并非单一表达式).

var (
    a = f() + v()
    b = g()
    c = sqr(u()) + v()
)

1).根据包变量初始化规则.初始化过程按照"a->b->c"顺序进行"ready for initialization"变量查找.

2).第一轮:变量a依赖c.b符合条件.b被初始化依赖普通求值顺序规则.g被调用.

3).第二轮:变量c符合条件.c被选出初始化.依据普通求值顺序规则,u sqr v先后被调用.

4).第三轮:变量a符合条件.a被选出初始化.依据普通求值顺序规则.f v先后被调用.

5).综合以上分析.得出调用顺序:g->u->sqr->v->f->v.

执行结果:

3.赋值语句求值:

n0 , n1 = n0 + n1 , n0

//或者 

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

这是一个赋值语句.Go语言规定.赋值语句求值分为两个阶段.

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

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

根据上述规则.对这个问题等号两端的表达式的操作数采用从左到右的求值顺序.

假定n0和n1的初始值如下.

n0 , n1 = 1 , 2

1).第一阶段:等号两端表达式求值.上述问题中.等号左边没有需要求值的下标表达式 指针解引用表达式等.只有右端有

n0 + n1 和 n0两个表达式.但表达式的操作数(n0 , n1)都是已初始化的.因此直接将值代入.得到求值结果.求值后.语句可以看成n0,n1=3,1.

2).第二阶段.从左到右赋值.即n0=3,n1=1.

示例:

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

func main() {
    example()
}

执行结果:

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

先看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("enter into case1")
       fallthrough
    case expr(4):
       fmt.Println("enter into case2")
    }
}

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

2).接下来将按照从上到下 从左到右的顺序对case语句中的表达式进行求值.如果某个表达式的结果与switch表达式结果一致.那么求值停止.后面未求值的case表达式将被忽略.

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

执行结果:

Go语言中的select语句为我们提供了一种在多个channel间实现多路复用的机制.是编写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 getAWriteChannel() chan<- int {
    fmt.Println("invoke getAWriteChannel")
    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")
    case getAWriteChannel() <- getANumToChannel():
       fmt.Println("send something to a writeonly channel")
    }
}

1).select在执行的时候.首先所有case表达式都会按出现的先后顺序求值一遍.

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

2).如果选择要执行的是一个从channel接收数据的case.那么该case等号左边的表达式在接收前才会被求值.上面的例子中.在getAReadOnlyChannel创建的goroutine在3s后向channel中写入一个int值后.select选择了第一个case执行.此时对等号左侧的表达式(getASlice())[0]进行求值.输出"invoke getASlice".也算是一种惰性求值.

执行结果:

到此这篇关于Go语言表达式的求值顺序的文章就介绍到这了,更多相关Go语言表达式求值顺序内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 如何解析golang中Context在HTTP服务中的角色

    如何解析golang中Context在HTTP服务中的角色

    这篇文章主要介绍了如何解析golang中Context在HTTP服务中的角色问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • 利用Golang实现TCP连接的双向拷贝详解

    利用Golang实现TCP连接的双向拷贝详解

    公司中遇到了一个使用golang编写的agent程序,所以这篇文章主要给大家介绍了关于利用Go如何实现TCP连接的双向拷贝的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考,下面随着小编来一起看看吧。
    2017-09-09
  • 学习GO编程必备知识汇总

    学习GO编程必备知识汇总

    这篇文章主要介绍了学习GO编程必备知识汇总的相关资料,需要的朋友可以参考下
    2016-07-07
  • golang实现数组分割的示例代码

    golang实现数组分割的示例代码

    本文主要介绍了golang实现数组分割的示例代码,要求把数组分割成多个正整数大小的数组,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Go语言学习之文件操作方法详解

    Go语言学习之文件操作方法详解

    这篇文章主要为大家详细介绍了Go语言中一些常见的文件操作,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的可以参考一下
    2022-04-04
  • Golang实现DFA算法对敏感词过滤功能

    Golang实现DFA算法对敏感词过滤功能

    DFA算法是确定性有限自动机,其特征是,有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,通俗的讲DFA算法就是把你要匹配的做成一颗字典树,然后对你输入的内容进行匹配的过程,本文将利用DFA算法实现敏感词过滤,需要的可以参考一下
    2023-10-10
  • 详解Golang如何实现节假日不打扰用户

    详解Golang如何实现节假日不打扰用户

    这篇文章主要为大家介绍了Golang如何实现节假日不打扰用户过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • Go语言依赖管理三要素示例解析

    Go语言依赖管理三要素示例解析

    这篇文章主要介绍了Go语言依赖管理三要素及示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • Go实现将任何网页转化为PDF

    Go实现将任何网页转化为PDF

    在许多应用场景中,可能需要将网页内容转化为 PDF 格式,使用Go编程语言,结合一些现有的库,可以非常方便地实现这一功能,下面我们就来看看具体实现方法吧
    2024-11-11
  • Go 跨域中间件解决CORS问题

    Go 跨域中间件解决CORS问题

    跨域问题CORS是前端开发人员经常遇到的问题,本文主要介绍了Go 跨域中间件解决CORS问题,具有一定的参考价值,感兴趣的可以了解一下
    2025-04-04

最新评论