赞
踩
作用域和生命期的概念可以借鉴 C 语言,但还有一些不太一样的地方,需要单独解释。
任何一门高级计算机语言都有作用域的概念,go 也不例外。
说到作用域,必然也会想到生命期。有些同学可能会把作用域和生命期划上了等号,比如在 c 语言里,在函数中声明了局部变量 int x
,这个 x
的作用域就在函数体内,一旦执行完此函数,x
也就销毁了。
实际上,作用域和生命期有着本质的区别:
换句话说,作用域限制了 object 的可见范围。比如在某个函数里声明了变量 int x
,这个 x
就只能在这个函数里可见,在函数外面是无法使用它的,这一点是编译器做出的限制。
如果你真的想在函数外面使用 x
,可不可以?在 c 语言里其实可以通过指针将 x
地址传出去,不过即使你有机会去修改它,可能会面临程序 core dump 的风险。
但是在 go 里我们知道,是允许将局部变量的地址传到外面进行访问的。这意味着 x
的生命期被延长。
其实你可以跳过这一节,但是最好不要。在讲作用域前,先来明确语法块和词法块的概念。
在 go 里,使用花括号括起来的部分,称为语法块。比如函数体,循环体。
func f() {
var a int = 5
var b int = 6
var c int
c = a + b
}
语法块内部声明的变量对外部是不可见的。
包含了一组声明和语句的代码片段,称为词法块(不一定非得要花括号)。比如,语法块是词法块的一个特例。还有一个特殊的例子就是整个程序所构成的源码也是一个词法块,它比较特殊,有个单独的名字,叫全局词法块。
还有很多例子,比如一个 package 构成包词法块,一个文件构成文件词法块,for、if、switch 语句所包含的词法块。
你需要注意的就是词法块的概念比语法块更大,语法块只是词法块的一种。
那么问题来了,作用域是什么?首先明确一下,作用域表示的是范围,接下来就是范围大小的问题。
注意:go 的 goto 和 c 的一样,后面跟着标签。go 的 break 和 continue 还有另外一种用法,就是 break 和 continue 后面也可以加标号,用在 for 循环里。这是一种扩展的语法,以后遇到了再说。
当编译器查找一个名字的时候,首先从最内层的作用域开始查找,一层一层往外找,直到全局作用域。如果最后在全局作用域还找不到,编译器报错。
下面的例子都在目录 gopl/programstructure/scope
下面。下面的 4 个例子非常重要,请务必运行一遍。
// demo01.go
package main
import "fmt"
var g int = 100
func test() {
var x int = 5
fmt.Println(x) // ok
}
func main() {
var local int = 10
fmt.Println(g) // ok
fmt.Println(local) // ok
fmt.Println(x) // not ok
}
// demo02.go
package main
import "fmt"
func main() {
var x = "hello!"
for i := 0; i < len(x); i++ {
var x = x[i] // 这两个 x 位于不同词法块,作用域也不同
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO"
}
}
}
上面的 for 语句创建了两个作用域:
也就是说,作用域 2 包含了作用域 1. (if else
条件语句也和 for
类似).
package main
import "fmt"
func main() {
if x := 5; x == 0 {
fmt.Println(x)
var z = 8
} else if y := 6; y == 0 {
fmt.Println(x)
} else {
fmt.Println(x)
fmt.Println(y)
fmt.Println(z) // not ok
}
}
分析一下,第一个 if 创建了三个作用域。
这很难看出来,但是上面的程序实际就是下面的程序的简写,我相信看完下面的代码,你就明白了:
// demo04.go
package main
import "fmt"
func main() {
if x := 5; x == 0 {
fmt.Println(x)
var z = 8
} else {
if y := 6; y == 0 {
fmt.Println(x)
} else {
fmt.Println(x)
fmt.Println(y)
fmt.Println(z) // not ok,z 在这里不可见
}
}
}
// demo05.go
package main
import "fmt"
func main() {
fmt.Println(g) // ok,尽管 g 声明在后面,但是这里仍然可以使用它
fmt.Println(local) // not ok.
var local int = 10
}
var g int = 100
这个例子想说明的是,包级声明的变量,顺序是无头紧要的,你可以在声明语句的上方就使用它。但是比包级作用域更小的作用域内声明变量就必须要按照顺序来(这种变量称为局部变量),一个变量必须要在使用前声明,否则会报错。
生命期是运行时的概念。
对于包级变量来说,它的生命期和整个程序的运行周期是一致的。程序运行结束,包级变量的生命期也随之结束。
但是局部变量的生命期是动态的:从变量被创建开始,直到该变量没有被引用为止。但是变量没有被引用,不意味着它的内存会被立即回收,可能会有一点延时,这取决于 go 的垃圾回收算法实现。
在 go 里,函数里的局部变量不一定是在栈上分配内存,使用 new 也不一定就非得在堆上分配内存。
举个例子:
var global *int
func f() {
var x int = 1 // 这个 x 必须在堆上来分配内存
global = &x
}
func g() {
y := new(int) // 这个 y 可以在堆上分配内存,也可以在栈上分配内存
*y = 1
}
上面的 x
变量将自己的地址赋值到全局变量 global 上
,这意味着在函数外面也能访问到 x
. 没错, x
的生命期被延长为了整个程序的生命周期。
用 go 的专业术语是这样说的:x
escapes from f
,翻译成中文是:x
从函数 f
中逃逸。
而变量 *y
并未发生逃逸,所以 *y
可能在栈上分配内存,也可能在堆上分配内存。
这一篇可能是你学习以来最难的一篇了,不要担心,多读几遍,理解下面几个关键的概念就算 ok.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。