变量

变量与常量

变量声明

Go 语言里面定义变量有多种方式。使用 var 关键字是 Go 最基本的定义变量方式,与 C 语言不同的是 Go 把变量类型放在变量名后面:

// 定义一个名称为 variableName,类型为"type"的变量
var variableName type

// 定义三个类型都是 type 的变量
var vname1, vname2, vname3 type

// 初始化“variableName”的变量为“value”值,类型是“type”
var variableName type = value

/*
定义三个类型都是"type"的变量,并且分别初始化为相应的值 vname1 为 v1,vname2 为 v2,vname3 为 v3
*/
var vname1, vname2, vname3 type= v1, v2, v3

// 定义多个变量
var (
	home   = os.Getenv("HOME")
	user   = os.Getenv("USER")
	gopath = os.Getenv("GOPATH")
)

/*
 定义三个变量,它们分别初始化为相应的值 vname1 为 v1,vname2 为 v2,vname3 为 v3 编译器会根据初始化的值自动推导出相应的类型
*/
vname1, vname2, vname3 := v1, v2, v3

:= 这个符号直接取代了 vartype,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用 var 方式来定义全局变量。_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值 35 赋予 b,并同时丢弃 34

_, b := 34, 35

Go 对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i但未使用。

package main

func main() {
  var i int
}

重新声明与再次赋值

调用了 os.Open 的声明为

f, err := os.Open(name)

该语句声明了两个变量 f 和 err。在几行之后,又通过

d, err := f.Stat()

调用了 f.Stat。它看起来似乎是声明了 d 和 err。注意,尽管两个语句中都出现了 err,但这种重复仍然是合法的:err 在第一条语句中被声明,但在第二条语句中只是被再次赋值罢了。也就是说,调用 f.Stat 使用的是前面已经声明的 err,它只是被重新赋值了而已。

在满足下列条件时,已被声明的变量 v 可出现在 := 声明中:

  • 本次声明与已声明的 v 处于同一作用域中(若 v 已在外层作用域中声明过,则此次声明会创建一个新的变量§),
  • 在初始化中与其类型相应的值才能赋予 v
  • 此次声明中至少另有一个变量是新声明的

当在有嵌套关系的作用域内声明了相同名的值时,就产生了所谓的 Variable Shadow:

i := 1
{
	i := i
	i = i * 2
	fmt.Println(i) // 2
}
fmt.Println(i) // 1

花括号内为新的作用于,在其中重新声明了变量 i。此时花括号内的新变量 i,shadow 了外面作用域的 i。

变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。变量的生命周期与变量的作用域有着不可分割的联系:

  • 全局变量:它的生命周期和整个程序的运行周期是一致的;
  • 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
  • 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
for t := 0.0; t < cycles*2*math.Pi; t += res {
    x := math.Sin(t)
    y := math.Sin(t*freq + phase)
    img.SetColorIndex(
        size+int(x*size+0.5), size+int(y*size+0.5),
        blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
    )               // 小括号另起一行缩进,和大括号的风格保存一致
}

上面代码中,在每次循环的开始会创建临时变量 t,然后在每次循环迭代中创建临时变量 x 和 y。临时变量 x、y 存放在栈中,随着函数执行结束(执行遇到最后一个}),释放其内存。

  • 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
  • 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号{ }中定义的局部变量。

在程序的编译阶段,编译器会根据实际情况自动选择在栈或者堆上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。

var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}

上述代码中,函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。用 Go 语言的术语说,这个局部变量 x 从函数 f 中逃逸了。相反,当函数 g 返回时,变量 *y 不再被使用,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由 Go 语言的 GC(垃圾回收机制)回收这个变量的内存空间。

在实际的开发中,并不需要刻意的实现变量的逃逸行为,因为逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。虽然 Go 语言能够帮助我们完成对内存的分配和释放,但是为了能够开发出高性能的应用我们任然需要了解变量的声明周期。例如,如果将局部变量赋值给全局变量,将会阻止 GC 对这个局部变量的回收,导致不必要的内存占用,从而影响程序的性能。

下一页