1.8 数据类型:指针
指针(Pointer)是 Go 语言中的重要概念,它存储变量的内存地址而非变量值本身。指针使得程序能够间接访问和修改内存中的数据,是实现高效内存管理、避免数据拷贝、实现复杂数据结构的基础。
理解指针对于掌握 Go 语言至关重要,它不仅影响程序性能,还是理解方法接收者、接口实现等高级特性的前提。
一、指针的基础概念
1. 什么是指针?
- 指针 (Pointer) 是一种特殊的变量,它存储了另一个变量在内存中的地址。
- 这个“另一个变量”就是指针所指向的目标。
2. Go 语言中的指针操作符
Go 语言主要使用两个操作符来操作指针:
| 操作符 | 名称 | 作用 | 示例 |
|---|---|---|---|
& | 取地址符 (Address-of) | 获取一个变量的内存地址。 | p := &v |
* | 取值符 (Dereference) | 根据地址获取该地址存储的值。 | v := *p |
二、指针的定义与使用
1. 指针变量的定义
指针变量的类型由它所指向的变量类型决定,语法为 *T,其中 T 是目标变量的类型。
go
var a int = 10
var p *int // 定义一个指向 int 类型的指针变量 p2. 基本操作步骤
| 步骤 | 代码示例 | 说明 |
|---|---|---|
| 获取地址 | p = &a | 将变量 a 的内存地址赋给指针 p。p 的值就是 a 的地址。 |
| 通过地址取值 | b := *p | 通过指针 p 存储的地址,取出该地址处存储的值(即 a 的值)。此时 b 等于 10。 |
| 修改指向的值 | *p = 20 | 通过指针 p 修改它所指向的变量 a 的值。此时 a 变为 20。 |
通过下面这段代码,你可以熟悉这两个符号的用法
go
package main
import "fmt"
func main() {
aint := 1 // 定义普通变量
ptr := &aint // 定义指针变量
fmt.Println("普通变量存储的是:", aint)
fmt.Println("普通变量存储的是:", *ptr)
fmt.Println("指针变量存储的是:", &aint)
fmt.Println("指针变量存储的是:", ptr)
}输出如下
go
普通变量存储的是: 1
普通变量存储的是: 1
指针变量存储的是: 0xc0000100a0
指针变量存储的是: 0xc0000100a0要想打印指针指向的内存地址,方法有两种
go
// 第一种
fmt.Printf("%p", ptr)
// 第二种
fmt.Println(ptr)3. 指针的类型
指针也是有类型的,一个指针只能指向特定类型的变量。指针类型的格式为 *T,其中 T 是指针所指向的数据类型。
go
package main
import "fmt"
func main() {
// 不同类型的指针
var intPtr *int // 指向 int 类型的指针
var strPtr *string // 指向 string 类型的指针
var boolPtr *bool // 指向 bool 类型的指针
var floatPtr *float64 // 指向 float64 类型的指针
// 为指针赋值
num := 42
text := "Hello"
flag := true
price := 99.99
intPtr = &num
strPtr = &text
boolPtr = &flag
floatPtr = &price
// 打印指针类型和值
fmt.Printf("intPtr 类型: %T, 值: %p, 指向的值: %d\n", intPtr, intPtr, *intPtr)
fmt.Printf("strPtr 类型: %T, 值: %p, 指向的值: %s\n", strPtr, strPtr, *strPtr)
fmt.Printf("boolPtr 类型: %T, 值: %p, 指向的值: %t\n", boolPtr, boolPtr, *boolPtr)
fmt.Printf("floatPtr 类型: %T, 值: %p, 指向的值: %.2f\n", floatPtr, floatPtr, *floatPtr)
}输出结果:
intPtr 类型: *int, 值: 0xc000010098, 指向的值: 42
strPtr 类型: *string, 值: 0xc000010110, 指向的值: Hello
boolPtr 类型: *bool, 值: 0xc000010118, 指向的值: true
floatPtr 类型: *float64, 值: 0xc000010120, 指向的值: 99.99指针类型的重要特性:
类型安全:不同类型的指针不能相互赋值
govar intPtr *int var strPtr *string // intPtr = strPtr // 编译错误:类型不匹配自定义类型的指针:
gotype Person struct { Name string Age int } var personPtr *Person // 指向 Person 结构体的指针 person := Person{Name: "Alice", Age: 30} personPtr = &person fmt.Printf("Person指针类型: %T\n", personPtr) // *main.Person fmt.Printf("Person信息: %+v\n", *personPtr) // {Name:Alice Age:30}指向指针的指针(多级指针):
gonum := 100 ptr := &num // *int 类型 ptrPtr := &ptr // **int 类型(指向指针的指针) fmt.Printf("num = %d\n", num) // 100 fmt.Printf("*ptr = %d\n", *ptr) // 100 fmt.Printf("**ptrPtr = %d\n", **ptrPtr) // 100 fmt.Printf("ptr类型: %T\n", ptr) // *int fmt.Printf("ptrPtr类型: %T\n", ptrPtr) // **int
4. 指针的零值
指针的零值是 nil。
- 当一个指针被定义后没有分配到任何变量时,它的值为
nil - 当一个指针为
nil时,表示它不指向任何有效的内存地址。 - 对
nil指针进行取值操作(解引用*p)会引发程序运行时错误(panic)。
go
package main
import "fmt"
func main() {
var p *string
fmt.Println(p)
fmt.Printf("p的值是%s/n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}
}三、new() 函数的使用
Go 语言提供了一个内置函数 new(),用于为 基本类型或复合类型(如结构体) 分配内存。
| 语法 | new(T) |
|---|---|
| 作用 | 为类型 T 分配内存,将其初始化为该类型对应的零值,并返回该内存的地址(即 *T 类型的指针)。 |
| 示例 | p := new(int) |
与 & 操作符的区别:
&操作符是获取一个已存在变量的地址。new()函数是新分配一块内存空间,并返回其地址。
go
// 方式一:使用 & (常用)
var v int = 10
p1 := &v
// 方式二:使用 new()
p2 := new(int) // *p2 初始值为 0
*p2 = 5 // 通过指针赋值四、指针在函数中的应用
指针在函数调用中尤其重要,用于实现对外部变量的 “传引用” 效果,从而实现对外部状态的修改,避免大型数据结构的值拷贝。
1. 传值 vs 传指针
| 方式 | 函数参数 | 效果 | 目的 |
|---|---|---|---|
| 传值 (Value) | func add(x int) | 函数内部操作的是参数的副本,不会影响外部变量。 | 保护外部变量不被修改。 |
| 传指针 (Pointer) | func modify(p *int) | 函数内部通过指针可以直接修改外部变量的值。 | 修改外部状态;避免大型数据拷贝。 |
2. 传指针示例
go
func modifyValue(x *int) {
*x = *x * 2 // 解引用并修改传入地址的值
}
func main() {
num := 5
// 传入 num 的地址
modifyValue(&num)
// 此时 num 的值已经被修改为 10
fmt.Println(num)
}3. 指针与切片
切片与指针一样,都是引用类型。如果我们想通过一个函数改变一个数组的值,有两种方法:
- 将这个数组的切片做为参数传给函数
- 将这个数组的指针做为参数传给函数
尽管二者都可以实现我们的目的,但是按照 Go 语言的使用习惯,建议使用第一种方法,因为第一种方法,写出来的代码会更加简洁,易读。
使用切片
go
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}使用指针
go
func modify(arr *[3]int) {
(*arr)[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}五、结构体与指针的特殊性
1. 结构体指针访问字段
如果有一个结构体指针,Go 语言允许使用 点号 (.) 直接访问结构体字段,而无需手动解引用。这是 Go 提供的语法糖。
go
type Person struct {
Age int
}
p := new(Person) // p 是 *Person 指针
p.Age = 30 // 语法糖:等价于 (*p).Age = 302. 方法中的指针接收者
如结构体教程中所述,方法中使用指针接收者 (p *T) 是 Go 语言实现状态修改和高效编程的核心机制。
go
package main
import "fmt"
type BankAccount struct {
owner string
balance float64
}
// 值接收者:尝试修改(但实际不会生效)
func (acc BankAccount) DepositWrong(amount float64) {
acc.balance += amount // 只修改副本,原始数据不变
}
// 指针接收者:可以修改原始结构体
func (acc *BankAccount) Deposit(amount float64) {
acc.balance += amount // 修改原始数据
}
// 指针接收者:返回账户信息
func (acc *BankAccount) String() string {
return fmt.Sprintf("账户所有者: %s, 余额: %.2f", acc.owner, acc.balance)
}
func main() {
// 创建银行账户
account := BankAccount{
owner: "张三",
balance: 1000.0,
}
fmt.Printf("初始状态: %s\n", account.String())
// 使用值接收者的错误存款方法
account.DepositWrong(500.0)
fmt.Printf("错误存款后: %s\n", account.String()) // 余额不变
// 使用指针接收者的正确存款方法
account.Deposit(500.0)
fmt.Printf("正确存款后: %s\n", account.String()) // 余额增加
// Go 语言的自动转换特性
accountPtr := &account
accountPtr.Deposit(100.0) // 指针调用指针方法
fmt.Printf("最终状态: %s\n", accountPtr.String())
}输出结果:
初始状态: 账户所有者: 张三, 余额: 1000.00
错误存款后: 账户所有者: 张三, 余额: 1000.00
正确存款后: 账户所有者: 张三, 余额: 1500.00
取款成功: 账户所有者: 张三, 余额: 1300.00
余额不足,取款失败: 账户所有者: 张三, 余额: 1300.00
当前余额: 1300.00
最终状态: 账户所有者: 张三, 余额: 1400.00关键要点:
- 值接收者 (
BankAccount):接收结构体的副本,无法修改原始数据 - 指针接收者 (
*BankAccount):接收结构体的指针,可以修改原始数据 - 自动转换:Go 会自动在值和指针之间进行转换,使调用更加便捷
六、使用指针的注意事项
- 避免悬空指针 (Dangling Pointer): 确保指针指向的内存区域仍然有效。
nil检查: 在对指针进行解引用操作前,务必检查它是否为nil,以防程序 panic。- 内存管理: Go 语言有 垃圾回收(GC) 机制,您不需要手动释放内存,这大大降低了 C/C++ 中指针导致的内存泄漏风险。