Go 语言指针
基本概念
Go 语言中支持通过指针来对变量内存地址直接访问和控制。
指针定义
计算机内存以字节(Byte)作为数据储存单位,每个字节配有唯一内存地址(Memory Address),存放内存地址的数据类型叫指针(Pointer)。
在声明变量时,会在内存中给它分配一个位置,以便能储存、修改和获取变量值。所有变量在程序运行时都有指针,变量实际数据第一个字节地址值为变量的指针。通过变量指针可以绕过变量名,间接读取或更新变量值。
指针类型
Go 语言中指针类型 uintptr
是一个特殊的无符号整数,大小根据操作系统的架构而不同:
- 32 位架构:占用 4 个字节,取址范围
0
到2^32 - 1
(约 4 GB),这是 32 位系统内存 4 GB 限制的由来。 - 64 位架构:占用 8 个字节,取址范围
0
到2^64 - 1
(约 16 EB,1EB = 1024 PB),近乎无上限。
指针限制
Go 语言指针有一系列设计层的限制:
-
没有指针运算:在很多低级语言中,允许直接操作内存地址,例如通过加减指针值来访问数组元素(连续内存块)。Go 语言中禁止这类操作,避免因指针运算导致数组越界访问、野指针等错误。
-
不能对常量或临时值取址:在 Go 语言中不能获取常量、字面量或表达式临时结果的地址。因为这些值没有固定地址,尝试取址将导致编译错误。
-
无法直接操作内存: 直接操作内存允许软件与硬件直接交互,如在嵌入式系统开发中,直接向特定内存地址的硬件寄存器写值。Go 语言不能通过指针直接操作内存地址,给指针变量赋值。
要在 Go 语言在操作内存,只能通过 unsafe
包来绕过类型安全执行指针操作。
语法糖
数组和结构体通过指针访问元素时,不需要显式解引用。这是 Go 语言中提供的语法糖(Syntactic sugar),以取代其他语言中的 ->
运算符。语法糖仅作用于简化编写代码,实际运行时还是会自动解引用:
package main import "fmt" // Person 为结构体类型 type Person struct { Name string Age int } func main() { // 数组示例 pArr := &[3]int{10, 20, 30} // 修改数组元素时,无需先解引用,编译器会翻译为 (*pArr)[0] = 100 pArr[0] = 100 fmt.Println("通过指针访问数组元素:", pArr[0]) // 结构体示例 pBob := &Person{"Bob", 25} pBob.Age++ // 实际等同于 (*pBob).Age++ fmt.Println("通过指针访问结构体字段:", pBob.Age) // 基本数据类型示例 num := 100 //pNum := &100 // 报错:无法提取常量地址 pNum := &num // 修改指针指向值,必须显式解引用 *pNum = 200 // 必须解引用来访问值,否则打印指针地址 fmt.Println("通过解引用指针访问值:", *pNum, "原变量:", num) }
指针应用
由于 Go 语言中指针操作很安全,因此在程序中使用得非常广泛。
声明和初始化
指针类型使用 *
符号跟在其他类型之前来表示:
var PointerName *PointerType
-
PointerName
:指针变量名称。一般前缀为「p」、「ptr」或添加后缀「Ptr」。 -
PointerType
:指针指向的变量类型,前面加上*
表示这是一个指向该类型的指针。
指针类型有 3 种常用声明和初始化方式:
package main import "fmt" func main() { // 在类型前加上星号,完整声明一个 float64 类型空指针 var pn *float64 // 声明但未初始化 fmt.Println(pn) // 打印指针默认值 <nil> // 使用 new 函数创建并初始化一个指针变量,值为类型零值 pi := new(int) fmt.Printf("地址:%p 值:%v\n", pi, *pi) // 使用短变量声明数组和结构体类型指针 pl := &[2]string{"a", "b"} fmt.Printf("地址:%p 值:%v\n", pl, *pl) }
变量取址
使用 &
操作符可以获取一个变量内存地址,也就是指针:
package main import "fmt" func main() { s := "111" p := &s // 将指针赋给变量 // 指向相同地址,用十六进制表示,如 0xc0420dc1c0 fmt.Printf("变量值:%v,变量地址:%p\n", s, p) fmt.Printf("变量值:%v,变量地址:%p\n", s, &s) }
结构体成员或数组元素都是变量,所以也能取址。
指针取值
对指针变量使用 *
操作符来解引用,获得指针对应的值内容:
package main import "fmt" func main() { s := "111" p := &s // 使用 * 符号对指针进行解引用,获取指针所指向值 *p = "222" // 通过指针修改变量值 fmt.Println("通过指针获取变量值:", *p, s) }
传递指针
由于指针地址只占 4 或 8 个字节,因此用指针传递大体积数据时,会明显节省开销。像切片、映射、接口、通道这样的引用类型默认使用指针传递,结构体必要时也能用指针传递。
另外,传递指针也用于在函数内部修改外部变量的值:
package main import "fmt" func main() { var num int = 1 fmt.Println("main 函数 num 初始值为:", num) // 打印原值:1 fmt.Println("main 函数 num 地址为:", &num) // 打印变量地址 incs(num) // 通过值传递,传入变量 num 副本 fmt.Println("调用 incs 后,main 函数中 num 值不变:", num) incp(&num) // 通过指针传递,传入变量 num 地址 fmt.Println("调用 incp 后,main 函数中 num 值变为:", num) } // incs 通过值传递接收一个 int 参数,在内部修改它不影响原始变量 func incs(num int) int { fmt.Println("incs 函数中,num 地址为:", &num) // 打印不同地址 num++ return num } // incp 通过指针传递接收一个指针,会修改原始变量值 func incp(numPtr *int) int { fmt.Println("incp 函数中,num 地址为:", numPtr) // 打印相同地址 *numPtr++ return *numPtr }
在并发场景下,需要小心处理指针,避免竟态条件。
空指针
Go 语言中空指针值为 nil
,也叫 nil
指针。空指针不指向任何有效地址,解引用会致使程序崩溃。因此在使用指针前需要检查是否为 nil
:
package main import "fmt" func main() { var p *int // 默认为 nil //fmt.Println("p 的地址", *p) // 试图打印空指针地址时报错:内存地址不正确 // 为 p 分配一个值 //*p = 5 // 试图解引用空指针将导致运行时错误:空指针解引用 num := 5 p = &num // p 现在指向 num // 检查指针是否为 nil,安全地使用指针 if p == nil { fmt.Println("p 是空指针") return } fmt.Println(p, *p) }
多级指针
多级指针是指向指针的指针,通过在指针类型前再加上 *
符号来表示:
package main import "fmt" // modify 函数接收多级指针,间接修改指针指向值 func modify(ptr **int) { **ptr = 100 } func main() { a := 20 pa := &a // pa 是普通指针 ppa := &pa // ppa 是二级指针 // 都输出 20 fmt.Println("a 值 =", a) fmt.Println("pa 值 =", *pa) fmt.Println("ppa 值 =", **ppa) // 调用函数,通过多级指针修改 a 值 modify(ppa) // 都输出新值 100 fmt.Println("修改后:\na 值 =", a) fmt.Println("pa 值 =", *pa) fmt.Println("ppa 值 =", **ppa) }
在 Go 语言中多级指针比较少见,适用于构建自定义复杂数据结构,如链表或树。
逃逸分析
逃逸分析(Escape Analysis)是一种内存优化技术,指编译器对变量存放位置进行分析。函数内局部变量会在栈上分配,函数运行结束后自动清除。但如果局部变量逃到函数外部,生命周期不再和函数绑定,那这个变量就会分配到堆上,由垃圾回收器跟踪回收。
逃逸情形
常会引发逃逸的情况如下:
- 返回局部变量指针:函数返回指向其局部变量指针,这个变量在函数执行完后,还能继续访问。
- 发送指针到通道:通过通道发送指针或包含指针的结构体,接收者能在函数执行完毕后,继续访问这些数据。
- 闭包引用局部变量:函数闭包中引用的局部变量可能会逃逸,闭包在函数返回后还能调用。
变量逃逸
下面通过函数返回局部变量指针,导致变量逃逸:
package main import "fmt" func main() { // 将函数返回指针赋值,打印出和函数内部一样地址 p := getP() fmt.Printf("函数外地址:%p\n", p) fmt.Printf("函数外访问值:%d\n", *p) } // getP 创建一个整数变量,并返回变量指针 func getP() *int { n := 1 fmt.Printf("函数内地址:%p\n", &n) return &n }
堆上对象生命周期不由作用域控制,而是通过垃圾回收器管理。垃圾回收器定期运行,查找并清除不再被任何指针引用的对象。
逃逸检测
实际开发中要避免不必要的变量逃逸,减少额外内存分配和性能开销。编译时,可以在 go build
命令中添加 -gcflags="-m -l"
参数来检查逃逸分析结果。额外标志参数 -m
用于输出编译器优化细节,而 -l
会禁用函数内联优化:
D:\Software\Programming\Go\new>go build -gcflags="-m -l" # new ./main.go:14:2: moved to heap: n ./main.go:15:12: ... argument does not escape ./main.go:8:12: ... argument does not escape ./main.go:9:12: ... argument does not escape ./main.go:9:42: *p escapes to heap
上面结果显示变量 n
逃逸到堆上。