Go语言变量的生命周期与作用域

目录

生命周期和作用域

短变量声明语句的作用域

变量的作用域

全局作用域

函数作用域

块作用域

作用域的嵌套

同名变量的作用域

代码示例

总结


图片

更多关于Go的相关技术点,敬请关注公众号:CTO Plus后续的发文,有问题欢迎后台留言交流。

图片

 在公众号CTO Plus前面的文章中,我介绍到了变量(标识符)和常量的使用方法,并分别使用代码示例演示了他们的特性,具体可以参考文章《Go语言变量与标识符探秘:灵活存储数据》和《Go语言常量解密:恒定不变的值》。

那么,本篇文章我将来总结下变量的生命周期和作用域,内容包括:全局作用域、函数作用域、块作用域、作用域的嵌套、同名变量的作用域,以及对应的代码演示示例。

本文原文:Go语言变量的生命周期与作用域

图片

生命周期和作用域

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。

在任何编程语言中变量都是有他的生命周期和作用域的,不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域,它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用,是一个运行时的概念。

Go语言中,变量的生命周期指的是变量存在(生存)的时间。变量的生命周期可以分为静态生命周期和动态生命周期两种。

静态生命周期的变量在程序运行期间始终存在,例如全局变量和常量。

动态生命周期的变量在程序运行期间动态创建和销毁,例如局部变量和函数参数。

在函数中定义的局部变量和函数参数的生命周期与函数的调用次数有关,每次调用函数时都会重新创建这些变量和参数,函数返回时这些变量和参数也会被销毁。

控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。

短变量声明语句的作用域

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。

在这个程序中:

if f, err := os.Open(fname); err != nil { // compile error: unused: f    return err}f.ReadByte() // compile error: undefined ff.Close()    // compile error: undefined f

变量f的作用域只在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。

通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:

f, err := os.Open(fname)if err != nil {    return err}f.ReadByte()f.Close()

你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:

if f, err := os.Open(fname); err != nil {    return err} else {    // f and err are visible here too    f.ReadByte()    f.Close()}

但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。

var cwd string
func init() {    cwd, err := os.Getwd() // compile error: unused: cwd    if err != nil {        log.Fatalf("os.Getwd failed: %v", err)    }}

虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。

由于当前的编译器会检测到局部声明的cwd并没有使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。

var cwd string
func init() {    cwd, err := os.Getwd() // NOTE: wrong!    if err != nil {        log.Fatalf("os.Getwd failed: %v", err)    }    log.Printf("Working directory = %s", cwd)}

全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式:

var cwd string
func init() {    var err error    cwd, err = os.Getwd()    if err != nil {        log.Fatalf("os.Getwd failed: %v", err)    }}

变量的作用域

在Go语言中,变量的作用域指的是变量在程序中可见(可以被访问)的范围。变量的作用域决定了变量在哪些地方可以被访问和使用,变量的作用域可以分为全局作用域和局部作用域(函数作用域、块作用域)两种。了解变量的作用域对于编写可读性强、易于维护的代码非常重要。

在前面的文章《Go语言变量与标识符探秘:灵活存储数据》和《Go语言常量解密:恒定不变的值》中也分别使用到过全局作用域和局部作用域的变量。

全局作用域

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。

在函数体外部声明的变量具有全局作用域,全局作用域的变量可以在整个程序中的任何地方被访问。同时全局变量可以在任何函数中使用,包括其他文件中的函数。

package main
import "fmt"
// 全局变量var globalVar int = 100var age int
func main() {    fmt.Println(globalVar) // 输出:100    anotherFunc()}
func anotherFunc() {    fmt.Println(globalVar) // 输出:100}

在上面的代码中,我们定义了一个全局变量globalVar 和age,在main函数中可以直接访问它。

函数作用域

在函数体内部声明的变量具有函数作用域,只能在函数内部访问。这些变量称为局部变量,它们的作用域仅限于声明它们的函数。

package main
import "fmt"
func main() {    // 函数作用域    var localVar int = 200    fmt.Println(localVar) // 输出:200    anotherFunc()}
func anotherFunc() {    // fmt.Println(localVar) // 编译错误:undefined: localVar}

当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误(比如上面例子中的undefined: localVar)。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:

package main
import "fmt"
// 变量作用域var age int
func scopeFunc() {  fmt.Println(age) // 全局变量age 0  age = 30  fmt.Println(age) // 全局变量age 30  var age = 28  fmt.Println(age) // 28}
func scopeFunc2() {  fmt.Println(age) // 此处使用到的是全局变量age  30
  // 函数作用域 定义局部变量  var name = "steverocket"  blog := "https://mp.weixin.qq.com/s/0yqGBPbOI6QxHqK17WxU8Q"  fmt.Println(name) // steverocket  fmt.Println(blog) // https://mp.weixin.qq.com/s/0yqGBPbOI6QxHqK17WxU8Q} // 程序执行到此处  局部变量就会被释放
func scopeBlock() {  block1 := 123  block2 := 555  {    block1 := 456    {      block1 := 789      block3 := 333      {        block1 := 999        fmt.Println(block1) // 999        fmt.Println(block2) // 使用最外层的块作用域  555      }      fmt.Println(block1) // 789      fmt.Println(block3)    }    //fmt.Println(block3) // undefined: block3  外层块无法访问内层块作用域的变量    fmt.Println(block1) // 456  }  fmt.Println(block1) // 123}
func main() {  fmt.Println(age) // 全局变量age 0  scopeFunc()  scopeFunc2()  scopeBlock()  // 此处无法访问scopeFunc2中定义的两个局部变量 编译报错  //fmt.Println(name) // undefined: name  //fmt.Println(blog) //undefined: blog}

块作用域

代码块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。代码块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。对全局的源代码来说,存在一个整体的词法块,称为全局词法块(全局区);对于每个包,每个for、if和switch语句,也都有对应词法块,每个switch或select的分支也有独立的词法块,当然也包括显式书写的词法块(花括弧包含的语句)。

一个程序可能包含多个同名的声明,只要它们在不同的词法域(代码块内)就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。你可以将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。

在if语句、for循环等代码块内部声明的变量具有块作用域,只能在该代码块内部访问。这些变量的作用域仅限于声明它们的代码块。

package main
import "fmt"
func main() {    if x := 10; x > 5 {        // 块作用域        fmt.Println(x) // 输出:10    }    // fmt.Println(x) // 编译错误,无法访问其他代码块内部的变量:undefined: x}

作用域的嵌套

在Go语言中,作用域可以嵌套,内部作用域可以访问外部作用域的变量(同一级代码块内或者子代码块访问父级代码块)。但是外部作用域不能访问内部作用域的变量。

package main
import "fmt"
func main() {    outerVar := 100
    if true {        innerVar := 200        fmt.Println(outerVar, innerVar) // 输出:100 200    }
    // fmt.Println(innerVar) // 编译错误:undefined: innerVar}

在上述示例中,innerVar是在if语句的块作用域内部声明的,它可以访问外部作用域的outerVar变量,但是外部作用域无法访问innerVar变量。

在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。

func main() {    x := "hello!"    for i := 0; i < len(x); i++ {        x := x[i]        if x != '!' {            x := x + 'A' - 'a'            fmt.Printf("%c", x) // "HELLO" (one letter per iteration)        }    }}

在x[i]和x + 'A' - 'a'声明语句的初始化的表达式中都引用了外部作用域声明的x变量,稍后我们会解释这个。(注意,后面的表达式与unicode.ToUpper并不等价。)

正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句,还有一些隐含的规则。上面的for语句创建了两个词法域:花括弧包含的是显式的部分,是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分(i++),当然也包含循环体词法域。

下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域,只有两个块是显式创建的:

func main() {    x := "hello"    for _, x := range x {        x := x + 'A' - 'a'        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)    }}

和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:

if x := f(); x == 0 {    fmt.Println(x)} else if y := g(x); x == y {    fmt.Println(x, y)} else {    fmt.Println(x, y)}fmt.Println(x, y) // compile error: x and y are not visible here

第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。

同名变量的作用域

在不同的作用域中,可以使用相同的变量名,这些变量之间是相互独立的,互不影响。

package main
import "fmt"
var x int = 100
func main() {    fmt.Println(x) // 输出:100    foo()}
func foo() {    x := 200    fmt.Println(x) // 输出:200}

在上述示例中,全局作用域中的x变量和foo()函数中的x变量是不同的变量,它们之间没有关联。

代码示例

package main
import "fmt"
// 变量作用域var age int
func scopeFunc() {  fmt.Println(age) // 全局变量age 0  age = 30  fmt.Println(age) // 全局变量age 30  var age = 28  fmt.Println(age) // 28}
func scopeFunc2() {  fmt.Println(age) // 此处使用到的是全局变量age  30
  // 函数作用域 定义局部变量  var name = "steverocket"  blog := "https://mp.weixin.qq.com/s/0yqGBPbOI6QxHqK17WxU8Q"  fmt.Println(name) // steverocket  fmt.Println(blog) // https://mp.weixin.qq.com/s/0yqGBPbOI6QxHqK17WxU8Q} // 程序执行到此处  局部变量就会被释放
func scopeBlock() {  block1 := 123  block2 := 555  {    block1 := 456    {      block1 := 789      block3 := 333      {        block1 := 999        fmt.Println(block1) // 999        fmt.Println(block2) // 使用最外层的块作用域  555      }      fmt.Println(block1) // 789      fmt.Println(block3)    }    //fmt.Println(block3) // undefined: block3  外层块无法访问内层块作用域的变量    fmt.Println(block1) // 456  }  fmt.Println(block1) // 123}
func main() {  fmt.Println(age) // 全局变量age 0  scopeFunc()  scopeFunc2()  scopeBlock()  // 此处无法访问scopeFunc2中定义的两个局部变量 编译报错  //fmt.Println(name) // undefined: name  //fmt.Println(blog) //undefined: blog}

总结

通过本篇文章,我们已经看到包、文件、声明和语句如何来表达一个程序结构。我们也了解了变量的作用域可以帮助我们编写更清晰、易于维护的代码。合理使用作用域可以避免变量名冲突和不必要的全局变量,提高代码的可读性和可维护性。

最后我们来总结下变量在代码中的作用域:

  • 全局作用域的变量可以在整个程序中访问。

  • 函数作用域的变量只能在函数内部访问。

  • 块作用域的变量只能在代码块内部访问。

  • 作用域可以嵌套,内部作用域可以访问外部作用域的变量。

  • 同名变量在不同的作用域中是相互独立的,互不影响。

更多精彩,关注我公号,一起学习、成长

图片

推荐阅读:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SteveRocket

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值