这是我参与8月更文挑战的第 5 天,活动详情查看: 8月更文挑战
指针
指针的值是一个变量的地址。一个指针指示值所保存的位置。不是所有的值都有地址,但所有的变量都有。使用指针,可以在无需知道变量名字的情况下,间接读取或更新变量的值
如果一个变量声明为var x int,表达式&x(x的地址)获取一个指向整形变量的指针,它的类型是整形指针(* int)。如果值叫做p,就说p指向x,或者p包含x的地址。p指向的变量写成* p。表达式* p获取变量的值,一个整形,因为*p代表一个变量,所以它也可以出现在赋值操作的左边,用于更新变量的值
x := 1
p := &x // p是整形指针,指向x
fmt.Println(*p) //”1“
*p = 2 //等同于x = 2
fmt.Println(x) // 结果是2
每一个聚合类型变量的组成(结构体的成员或数组中的元素)都是变量,所以也有一个地址。指针类型的零值是nil。测试p≠nil,结果是true,说明p指向一个变量。指针是可以比较的,两个指针当且仅当指向同一个变量或者两者都是nil的情况下才相等
var x,y int
fmt.Printl(&x == &x, &x == &y, &x == nil) //true false false
函数返回局部变量的地址是非常安全的。示例,通过调用函数f产生的局部变量v,即使在调用返回后,依然存在,指针p依然引用它
var p = f()
func f() *int {
v := 1
return &v
}
每次调用f,都会返回一个不同的值
fmt.Println(f() == f()) //false
因为一个指针包含变量的地址,所以传递一个指针参数给函数,能够让函数更新间接传递的变量值。例如下边这个函数递增一个指针参数所指向的变量,然后返回此变量的新值
func incr(p *int) int{
*p++ //递增p所指向的值,p自身保持不变
return *p
}
v := 1
incr(&v)//v现在等于2
fmt.Println(incr(&v))// v现在是3
每次使用变量的地址或者复制一个指针,我们就创建了新的别名或者方式来标记同一变量。例如上边的*p是v的别名。指针别名允许我们不用变量的名字来访问变量,这一点是非常有用的,但它是双刃剑:为了找到所有访问变量的语句,需要知道所有的别名。不仅指针产生别名,当复制其它引用类型(像slice、map、通道,甚至包含这些引用类型的结构体、数组和接口)的时候,也会产生别名
new函数
另外一种创建变量的方式是使用内置的new函数。表达式new(T)创建一个未命名的T类型变量,初始化T类型的零值,并返回其地址(地址类型为*T)
p := new(int)//*int类型的p,指向未命名的int变量
fmt.Println(*p)//输出0
*p = 2//把未命名的int设置为2
fmt.Println(*p)//输出2
使用new创建的变量和取其地址的普通局部变量没有什么不同,只是不需要引入(和声明)一个虚拟的名字,通过new(T)就可以直接在表达式中使用。因此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
这个规则有一个例外:两个变量的类型不携带任何信息且是零值,比如struct{}或[0]int,这种情况他们有相同的地址
new是一个预声明的函数,不是一个关键字,所以它可以重定义为另外的其它类型,例如:
func delta(old, new int) int {
return new - old
}
变量的生命周期
生命周期指在程序执行过程中变量存在的时间段。包级别变量的生命周期是整个程序的执行时间。相反,局部变量有一个动态的生命周期:每次执行声明语句时创建一个新的实体,变量一直存在到它不可访问,这时它占用的存储空间会被回收。函数的参数和返回值也是局部变量,它们在函数被调用的时候创建
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)
}
变量t在每次for循环的开始创建,变量x和y在循环的每次迭代中创建
那么垃圾回收器如何知道一个变量是否应该被回收?说来话长,基本思路是每一个包级别的变量,以及每一个当前执行函数的局部变量,可以作为追溯该变量的路径的源头,通过指针和其它方式的引用可以找到变量。如果变量的路径不存在,那么变量变得不可访问,因此它不会影响任何其它的计算过程
因为变量的生命周期是通过它是否可达来确定的,所以局部变量可以在包含它的循环的一次迭代之外继续存活。即使包含它的循环已经返回,它的存在还可能延续
编译器可以选择使用堆或栈上的空间来分配,这个选择不是基于使用var或new关键字来声明变量
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
这里x一定是使用堆空间。因为它在f函数返回以后还可以通过global变量访问,尽管它被声明为一个局部变量。这种情况可以说成是x从f中逃逸。当g函数返回时,变量y变得不可访问,可回收。因为y没有从g中逃逸,所以编译器可以安全的在栈上分配*y的空间,即便使用的new函数创建它。在任何情况下,逃逸的概念使你不需要额外费心来写正确的代码,但要记住它在性能优化的时候是有好处的,因为每一次变量逃逸都需要一次额外的内存分配过程
垃圾回收对于写出正确的程序有巨大的帮助,但是免不了考虑内存的负担。不需要显式分配和释放内存,但是变量的生命周期是写出高效程序所必须清楚的。比如,在长生命周期对象中保持短生命周期对象不必要的指针,特别是在全局变量中,会阻止垃圾回收器回收短生命周期的对象空间
参考
《Go程序设计语言》—-艾伦 A. A. 多诺万
《Go语言学习笔记》—-雨痕