24.1 指针

一个指针变量可以指向任何一个值的内存地址。指针变量在 32 位计算机上占用 4B 内存,在 64 位计算机占用 8B内存,并且与它所指向的值的大小无关,因为指针变量只是地址的值而已。可以声明指针指向任何类型的值来表明它的原始性或结构性,也可以在指针类型前面加上*号(前缀)来获取指针所指向的内容。

Go语言中,指针类型表示指向给定类型(称为指针的基础类型)的变量的所有指针的集合。 符号 * 可以放在一个类型前,如 T,那么它将以类型T为基础,生成指针类型T。未初始化指针的值为nil。例如:

type Point3D struct{ x, y, z float64 }
var pointer *Point3D
var i *[4]int

上面定义了两个指针类型变量。它们的值为nil,这时对它们的反向引用是不合法的,并且会使程序崩溃。

xx := (*pointer).x
panic: runtime error: invalid memory address or nil pointer dereference

符号 * 可以放在一个指针前,如 (*pointer),那么它将得到这个指针指向地址上所存储的值,这称为反向引用。不过在Go语言中,(*pointer).x可以简写为pointer.x。

对于任何一个变量 var, 表达式 var == *(&var) 都是正确的。注意:不能得到一个数字或常量的地址,下面的写法是错误的:

const i = 5
ptr := &i // error: cannot take the address of i
ptr2 := &10 // error: cannot take the address of 10

虽然Go 语言和 C、C++ 这些语言一样,都有指针的概念,但是指针运算在语法上是不允许的。这样做的目的是保证内存安全。从这一点看,Go 语言的指针基本就是一种引用。

指针的一个高级应用是可以传递一个变量的引用(如函数的参数),这样不会传递变量的副本。当调用函数时,如果参数为基础类型,传进去的是值,也就是另外复制了一份参数到当前的函数调用栈。参数为引用类型时,传进去的基本都是引用。而指针传递的成本很低,只占用 4B或 8B内存。

如果代码在运行中需要占用大量的内存,或很多变量,或者两者都有,这时使用指针会减少内存占用和提高运行效率。被指向的变量保存在内存中,直到没有任何指针指向它们。所以从它们被创建开始就具有相互独立的生命周期。

内存管理中的内存区域一般包括堆内存(heap)和栈内存(stack), 栈内存主要用来存储当前调用栈用到的简单类型数据,如string,bool,int,float 等。这些类型基本上较少占用内存,容易回收,因此可以直接复制,进行垃圾回收时也比较容易做针对性的优化。 而复杂的复合类型占用的内存往往相对较大,存储在堆内存中,垃圾回收频率相对较低,代价也较大,因此传引用或指针可以避免进行成本较高的复制操作,并且节省内存,提高程序运行效率。

因此,在需要改变参数的值或者避免复制大批量数据而节省内存时(也会提高运行效率,毕竟大批量复制也耗费时间)都会选择使用指针

另一方面,指针的频繁使用也会导致性能下降。指针也可以指向另一个指针,并且可以进行任意深度的嵌套,形成多级的间接引用,但会使代码结构不清晰。

在大多数情况下,Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,如:自动反向引用。

指针的使用方法:

  • 定义指针变量;
  • 为指针变量赋值;
  • 访问指针变量中指向地址的值;
  • 在指针类型前面加上*号来获取指针所指向的内容。
package main
import "fmt"
func main() {
	var a, b int = 20, 30 // 声明实际变量
	var ptra *int         // 声明指针变量
	var ptrb *int = &b
	ptra = &a // 指针变量的存储地址
	fmt.Printf("a  变量的地址是: %x\n", &a)
	fmt.Printf("b  变量的地址是: %x\n", &b)
	// 指针变量的存储地址
	fmt.Printf("ptra  变量的存储地址: %x\n", ptra)
	fmt.Printf("ptrb  变量的存储地址: %x\n", ptrb)
	// 使用指针访问值
	fmt.Printf("*ptra  变量的值: %d\n", *ptra)
	fmt.Printf("*ptrb  变量的值: %d\n", *ptrb)
}
下一节:new() 和 make() 都在堆上分配内存,但是它们的行为不同,适用于不同的类型。