[Go]程序结构——变量

var声明语句可以创建一个特定类型的变量,然后为变量取一个名字,并设置变量的初始值。变量的一般声明语法如下:

var 变量名字 类型 = 表达式
其中“类型”或“=表达式”可以二选一。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息;如果省略的是初始化表达式,那么将根据类型信息,使用该类型的“零值”来初始化变量。以下是各种类型的零值:

  • 数值类型:0
  • 布尔类型:false
  • 字符串类型:""
  • 接口或引用类型:nil
  • 数组或结构体等聚合类型:是其元素或字段各自的零值

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如:

var s string
fmt.Println(s) // ""
这段代码将打印一个空字符串,而不是导致错误(类似C#的NullReferenceException)或产生不可预知的行为(类C的非预期数据)。Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。

也可以在一个声明语句中同时声明多个变量,或用一组初始化表达式声明并初始化多个变量。如果省略每个变量的类型,将可以声明多个类型不同的变量

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
简短变量声明

在函数内部,有一种称为简短变量声明语句的形式,可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式自动推导。

anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量。var形式的声明语句往往是用于需要显示指定变量类型的地方,或者因为变量稍后会被重新复制而初始值无关紧要的地方。

i := 100                  // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化多个变量:

i, j := 0, 1
但是这种同时声明多个变量的方式应该限制在可以提高代码可读性的地方,比如for语句的循环初始化语句部分。

记住“:=”是一个变量声明语句,而“=”是一个赋值操作符。也不要混淆多个变量的声明和元组的多重赋值,后者是将右边各个表达式值赋值给左边对应位置的各个变量:

i, j = j, i // 交换 i 和 j 的值
和普通var刑事的变量声明语句一样,简短变量声明语句也可以用在函数的返回值,如下

f, err := os.Open(name)
if err != nil {
    return err
}
// ...use f...
f.Close()
这里有一个比较微妙的地方:简短变量声明左边的变量可能并全是新声明的。如果有一些已经在相同的作用域内声明过,那么简短变量声明语句对这些已经声明过的变量就只有赋值操作。

in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
第二个err已经在前面声明过,所以只有赋值操作。

简短变量声明语句中必须至少要有一个新的变量,下面的代码将不能通过编译:

f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
解决办法是,第二个简短变量声明语句改为普通的多重赋值语句。

指针

一个变量对应一个内存空间。变量在声明时会被分配适当的内存空间,这个内存空间会被绑定到一个名称。

一个指针的值是一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。

通过指针,我们可以直接读/写对应的变量值,而不需要知道变量的名字(如果变量有名字的话)。

如果用“var x int”声明一个变量x,那么&x表达式将产生一个指向该变量的指针,指针对应的数据类型为*int(这与C的指针int*相反)。同时*p表达式对应变量x的值(这与C一致)。一般*p表示读取变量的值,但如果出现在赋值运算符左边,则与x的含义一致。

x := 1
p := &x         // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2          // equivalent to x = 2
fmt.Println(x)  // "2"
对于聚合类型的每个成员——比如结构的字段,数组的元素——也都对应一个变量,因此可以被取地址。

变量有时候被称为可寻址的值,即变量由表达式临时生成,也必须能接受&地址操作。

指针的零值为nil。如果指针指向某个有效的变量,那么p!=nil为真。指针之间也可以进行相等测试,只有当它们指向同一个变量或全部为nil时才相等。

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
在Go语言中,返回函数中的局部变量地址也是安全的(这与C不一致,因为C的局部变量是存放在栈上,函数返回后栈帧会被破坏)。例如:

var p = f()

func f() *int {
    v := 1 //编译器会将这个局部变量分配在堆上而不是栈上
    return &v
}
每次调用f函数都将返回不同的结果:

fmt.Println(f() == f()) // "false"
因为指针存放一个变量的地址,因此如果将指针作为函数参数,那么在函数内部对指针所指向的内存空间进行修改会反应在对应的变量上。

unc incr(p *int) int {
    *p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
    return *p
}

v := 1
incr(&v)              // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
new函数

另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量的地址,返回的指针类型为*T。

p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
用new创建的变量和普通变量声明语句 没有什么区别,换而言之,new函数类似一种语法糖,而不是一个新的基础概念。下面两个newInt函数有着相同的行为:

func newInt() *int {
    return new(int)
}

func newInt() *int {
    var dummy int
    return &dummy
}
每次调用new函数都会返回一个新的变量地址,因此下面两个地址不相等:

p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
当然也有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如 struct{} 和 [0]int,有可能有相同的地址(依赖具体的语言实现)(谨慎使用大小为0的类型,这可能导致Go语言垃圾回收器有不同的行为,具体查看runtime.SetFinalizer函数相关文档)。

new函数使用相对较少,因为对于结构体来说,直接用字面量语法会更加灵活。

由于new只是一个预定义的函数,它并不是关键字,因此可以将new重新定义,例如

func delta(old, new int) int { return new - old }
由于new被定义为int类型的变量名,因此在delta函数内部将无法使用内置的new函数。

变量声明周期

变量的生命周期指的是在程序运行期间变量有效存在时间间隔。对于在包级别的变量来说,它们的生命周期与程序运行周期一致。而局部变量的声明周期则是动态的:每次从创建的地方开始,知道该变量不在被引用,然后其存储空间可能被回收。如下

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)
}
注意,函数的右括号可以另起一行,同时为了防止编译器在行尾自动插入分号而导致编译错误,可以在末尾的参数后面显示插入逗号
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。

那么Go语言的垃圾收集器是如何知道一个变量可以被回收呢?其基本思路是:从每个包级别的变量和每个当前运行的函数的每个局部变量开始(类似.NET垃圾收集器中的‘根’概念),通过指针或引用的访问路径遍历,是否可找到该变量(类似.NET垃圾收集器中的‘可达对象’概念)。如果不存在这样的访问路径,那么说明变量是不可达的,也就被认为是垃圾。

因为一个变量的有效期只取决于是否可达(非包级别变量),因此一个循环迭代内部的局部变量的生存期可能超出其局部作用域(这是因为即使这些变量不可达,也不一定会立即被回收,因为此时垃圾收集器还没有开始回收工作)。

编译器会自动选择在栈上还是堆上分配局部变量的存储空间,但这个选择并不是由var和new来决定的。

var global *int

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

func g() {
    y := new(int)
    *y = 1
}
f函数的x变量必须在堆上分配,因为它在函数退出后依然可以通过包级别的变量global访问,虽然它在函数内部定义(这与C不一致)。Go语言术语:这个局部变量x从函数f中逃逸了。

相反,当g函数返回后,变量y将不再可达,这时就变为垃圾了,但不一定立刻被回收。这里需要注意的是,前面所描述的是y变量被分配在堆上的情况,只有堆上的对象才会被垃圾收集器回收。编译器可以选择将变量y分配在栈上,也可以选择分配在堆上。栈上的变量不需要回收,因为函数返回后,其使用的栈帧不再有效,栈帧中使用的局部变量自动被销毁。

所以程序员无法选择是在栈上还是堆上分配变量。

了解变量的分配位置和生命周期,对编写高效的程序时分重要

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值