指针基础
指针是存储另一个变量内存地址的变量。通过指针,可以直接访问和修改内存中的值。Go 语言保留了指针以提高程序效率,但移除了 C 语言中容易出错的指针运算功能,使指针更加安全。
& 与 * 运算符
Go 提供两个基本的指针运算符:
| 运算符 | 名称 | 作用 | 示例 |
|---|---|---|---|
& | 取地址运算符 | 获取变量的内存地址 | p := &x |
* | 解引用运算符 | 通过指针访问其指向的值 | y := *p |
基本用法
func main() {
x := 42
// & 取地址:获取 x 的内存地址
p := &x
fmt.Printf("x 的值: %d\n", x) // x 的值: 42
fmt.Printf("x 的地址: %p\n", p) // x 的地址: 0xc00001a0a8(每次运行可能不同)
fmt.Printf("p 的类型: %T\n", p) // p 的类型: *int
// * 解引用:通过指针访问其指向的值
fmt.Printf("*p 的值: %d\n", *p) // *p 的值: 42
// 通过指针修改值
*p = 100
fmt.Printf("x 的值: %d\n", x) // x 的值: 100
}理解指针的两个步骤
&x— 获取x的内存地址,结果是一个指针*p— 通过指针p访问它指向的值- 记忆口诀:
&是”取地址”,*是”取值”
指针的零值
指针的零值是 nil,表示不指向任何内存地址:
var p *int
fmt.Println(p) // <nil>
fmt.Println(p == nil) // true
// 注意:对 nil 指针解引用会导致 panic
// *p = 42 // panic: runtime error: invalid memory address or nil pointer dereferencenil 指针解引用会 panic
对 nil 指针使用 * 解引用会导致程序崩溃。在使用指针之前,应先检查是否为 nil。
func safeDeref(p *int) int {
if p == nil {
return 0 // 或返回错误
}
return *p
}指针作为函数参数
Go 中所有函数参数都是值传递。传递指针时,实际上传递的是地址值的拷贝,但通过这个拷贝的地址仍然可以修改原始数据。这实现了类似”引用传递”的效果,但本质上仍然是值传递。
值传递的问题
// 值传递:函数接收的是参数的副本
func tryDouble(x int) {
x = x * 2 // 只修改了副本,不影响原始变量
}
func main() {
num := 10
tryDouble(num)
fmt.Println(num) // 10(没有变化!)
}使用指针参数修改原始值
// 指针参数:函数接收地址的副本,通过地址修改原始值
func double(x *int) {
*x = *x * 2 // 通过指针修改原始值
}
func main() {
num := 10
double(&num) // 传递 num 的地址
fmt.Println(num) // 20(原始值被修改了!)
}指针参数的实际应用
// 1. 交换两个变量
func swap(a, b *int) {
*a, *b = *b, *a
}
func main() {
x, y := 10, 20
swap(&x, &y)
fmt.Println(x, y) // 20 10
}
// 2. 修改结构体字段
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age++ // 通过指针修改结构体
}
func main() {
user := User{"Alice", 25}
birthday(&user)
fmt.Printf("%+v\n", user) // {Name:Alice Age:26}
}
// 3. 避免大结构体的拷贝
func processLargeData(data *[]byte) {
// 传递切片的指针,避免拷贝底层数组
(*data)[0] = 'H'
}什么时候使用指针参数
- 需要修改调用者的变量时
- 传递大型结构体以避免拷贝开销时
- 函数需要返回多个修改结果时
- 需要表示”可能不存在”的值时(
*T可以为nil)
new() 函数
new(T) 是 Go 的内置函数,它为类型 T 分配零值内存,并返回指向该内存的指针 *T。等价于 var p *T = new(T) 或 p := new(T)。
基本用法
// 使用 new() 创建指针
p := new(int) // 分配一个 int 大小的内存,初始值为 0
fmt.Println(*p) // 0
fmt.Println(p) // 0xc00001a0a8(内存地址)
*p = 42
fmt.Println(*p) // 42new() 与变量声明的对比
// 方式 1:var 声明
var p1 *int = new(int)
// 方式 2:简短声明
p2 := new(int)
// 方式 3:先声明变量,再取地址
var x int
p3 := &x
// 以上三种方式 p1、p2、p3 都是指向 int 的指针new() 与结构体
type Point struct {
X, Y float64
}
// 使用 new()
p := new(Point)
fmt.Printf("%+v\n", p) // &{X:0 Y:0}
// 等价于
// var p *Point = new(Point)
// 更常用的方式:使用结构体字面量
p2 := &Point{X: 3.0, Y: 4.0}
fmt.Printf("%+v\n", p2) // &{X:3 Y:4}new() vs 取地址 &
new(int)返回指向零值int的指针&x返回已有变量x的地址&Point{X: 3, Y: 4}返回指向结构体字面量的指针(更常用)- 三者都会在栈或堆上分配内存(Go 编译器自动决定)
new() 与 make() 的区别
new() 不能用于引用类型
new() 只返回指向零值的指针。对于切片、映射、通道等引用类型,应使用 make() 进行初始化:
// new() 对于切片、映射、通道不太实用
p := new([]int) // p 是 *[]int,指向一个 nil 切片
// (*p)[0] = 1 // panic:不能对 nil 切片进行操作
// 正确做法:使用 make()
s := make([]int, 5) // s 是 []int,长度为 5,元素为零值
s[0] = 1 // 正确
m := make(map[string]int) // 创建并初始化 map
m["key"] = 1 // 正确
ch := make(chan int, 10) // 创建带缓冲的通道| 特性 | new(T) | make(T) |
|---|---|---|
| 返回类型 | *T(指针) | T(值本身) |
| 初始值 | 类型零值 | 已初始化(可立即使用) |
| 适用类型 | 所有类型 | 仅 slice、map、chan |
| 典型用途 | 创建结构体指针 | 创建引用类型 |
Go 指针 vs C 指针
Go 保留了指针以提高效率,但做了重大简化以确保安全性:
| 特性 | Go 指针 | C 指针 |
|---|---|---|
| 指针运算 | ❌ 不支持 | ✅ 支持 p++、p+1 等 |
| 指针算术 | ❌ 不支持 | ✅ 支持任意偏移量运算 |
| 强制类型转换 | ❌ 不允许 | ✅ 允许 int* 转 char* |
| 空指针检查 | 编译器部分检查 | 完全依赖程序员 |
| 垃圾回收 | ✅ 由 GC 管理 | ❌ 需手动管理 |
| 安全性 | 高 | 低(容易产生内存问题) |
Go 指针的设计哲学
Go 的指针设计原则是”保留效率,去除危险”。指针可以传递数据的地址以提高性能,但不能进行算术运算,避免了缓冲区溢出等安全问题。
不支持指针运算
arr := [5]int{10, 20, 30, 40, 50}
p := &arr[0]
// 以下操作在 Go 中都是非法的:
// p++ // 编译错误:cannot increment p
// p + 1 // 编译错误
// *(p + 2) // 编译错误
// 正确做法:使用切片和索引
fmt.Println(arr[2]) // 30不支持指针强制转换
var x int = 42
var p *int = &x
// 以下操作在 Go 中都是非法的:
// var pp *float64 = (*float64)(p) // 编译错误
// var n int = int(p) // 编译错误
// 正确做法:使用显式类型转换
var y float64 = float64(x) // 值的类型转换,不是指针转换unsafe 包
Go 提供了 unsafe 包可以绕过类型安全限制,执行指针运算和类型转换。但使用 unsafe 会失去编译器的类型检查保护,应仅在绝对必要时使用,一般用于与 C 代码交互或极度性能敏感的场景。
import "unsafe"
// 仅作演示,不推荐在生产代码中使用
var x int = 42
var p *int = &x
var pFloat *float64 = (*float64)(unsafe.Pointer(p))
// *pFloat 可以访问 x 的内存,但这非常危险!值传递 vs 引用传递
Go 的参数传递机制
Go 中所有的参数传递都是值传递。无论是基本类型、结构体还是指针,传递的都是值的拷贝。
值传递的本质
即使是传递指针,传递的也是地址值的拷贝。只不过这个拷贝的地址指向同一块内存,所以通过它可以修改原始数据。这并不是真正的”引用传递”。
值传递示例
type Person struct {
Name string
Age int
}
// 值传递:接收结构体的副本
func modifyValue(p Person) {
p.Name = "Modified" // 只修改副本
p.Age = 100
}
// 指针传递(本质还是值传递,传递的是地址的副本)
func modifyPointer(p *Person) {
p.Name = "Modified" // 通过地址修改原始数据
p.Age = 100
}
func main() {
person := Person{Name: "Alice", Age: 25}
modifyValue(person)
fmt.Printf("%+v\n", person) // {Name:Alice Age:25}(未改变)
modifyPointer(&person)
fmt.Printf("%+v\n", person) // {Name:Modified Age:100}(已改变)
}内存模型图解
值传递:
┌─────────────────┐ ┌─────────────────┐
│ main() │ │ modifyValue() │
│ │ │ │
│ person ─────────┼────────>│ p (副本) │
│ {Name:Alice │ │ {Name:Alice │
│ Age:25} │ │ Age:25} │
└─────────────────┘ └─────────────────┘
两个独立的内存空间
指针传递:
┌─────────────────┐ ┌─────────────────┐
│ main() │ │ modifyPointer() │
│ │ │ │
│ person ─────────┼──┐ │ p (地址副本) │
│ {Name:Alice │ └─────>│ ────────┐ │
│ Age:25} │ │ │ │
└─────────────────┘ └─────────┼───────┘
│
通过地址修改同一块内存Go 中的”引用类型”
Go 中有一些类型在传递时看似”引用传递”,但它们的本质是包含底层引用的结构:
| 类型 | 描述 | 传递行为 |
|---|---|---|
slice | 切片 | 传递包含指针的 slice header |
map | 映射 | 传递指向底层哈希表的指针 |
channel | 通道 | 传递指向底层队列的指针 |
interface | 接口 | 传递包含类型和数据的 iface 结构 |
// 切片传递
func modifySlice(s []int) {
s[0] = 999 // 可以修改元素(因为 slice header 中有底层数组的指针)
s = append(s, 4) // append 可能创建新数组,不影响原始切片
}
func main() {
nums := []int{1, 2, 3}
modifySlice(nums)
fmt.Println(nums) // [999 2 3](第一个元素被修改了)
}切片传递的陷阱
切片在函数内部的修改可能不会反映到外部:
- 修改已有元素:会反映(因为共享底层数组)
append导致扩容:不会反映(创建了新的底层数组)- 如果需要确保修改反映到外部,传递切片的指针
*[]int
// 安全的做法:传递切片指针
func safeAppend(s *[]int, val int) {
*s = append(*s, val)
}
func main() {
nums := []int{1, 2, 3}
safeAppend(&nums, 4)
fmt.Println(nums) // [1 2 3 4]
}练习题
练习 1:指针基础操作
以下代码的输出是什么?请逐步分析。
package main
import "fmt"
func main() {
a := 10
b := 20
p1 := &a
p2 := &b
*p1 = *p2
p2 = p1
fmt.Println(a, b)
fmt.Println(*p1, *p2)
}解题思路:分步跟踪每个变量的值。
分析:
a := 10, b := 20→ a=10, b=20p1 := &a→ p1 指向 ap2 := &b→ p2 指向 b*p1 = *p2→ 通过 p1 修改 a 的值为 b 的值(20),所以 a=20p2 = p1→ p2 现在也指向 a(b 没有被修改,仍为 20)
输出:
20 20
20 20解释:
*p1 = *p2:将p2指向的值(20)赋给p1指向的内存位置(即a),所以a变为 20p2 = p1:将p1的地址值赋给p2,p2现在指向a- 最终
a=20,b=20(b始终没有被修改),*p1=20,*p2=20(都指向a)
练习 2:指针参数交换值
编写函数 rotate3(a, b, c *int),将三个变量的值循环左移。例如原始值为 (1, 2, 3),调用后变为 (2, 3, 1)。
解题思路:通过指针参数直接修改三个变量的值,使用临时变量暂存一个值。
代码:
package main
import "fmt"
func rotate3(a, b, c *int) {
temp := *a // 暂存 a 的值
*a = *b // a ← b
*b = *c // b ← c
*c = temp // c ← 原来的 a
}
func main() {
x, y, z := 1, 2, 3
fmt.Printf("交换前: x=%d, y=%d, z=%d\n", x, y, z)
rotate3(&x, &y, &z)
fmt.Printf("交换后: x=%d, y=%d, z=%d\n", x, y, z)
// 交换后: x=2, y=3, z=1
// 再执行一次
rotate3(&x, &y, &z)
fmt.Printf("再次交换: x=%d, y=%d, z=%d\n", x, y, z)
// 再次交换: x=3, y=1, z=2
}关键点:利用临时变量存储需要覆盖的值,然后依次赋值,完成三变量的循环左移。
练习 3:new() 与值传递
以下代码的输出是什么?解释 new() 和值传递的行为。
package main
import "fmt"
func modify(p *int) {
p = new(int) // p 指向了新的内存
*p = 100
}
func main() {
x := 42
modify(&x)
fmt.Println(x)
}输出:
42分析:
x := 42→ x 存储值 42modify(&x)→ 将x的地址传递给modify函数- 在
modify函数内部:- 参数
p接收到的是x地址的副本 p = new(int)→ 让局部的p指向新的内存地址(x的地址副本被覆盖了)*p = 100→ 修改的是新分配的内存,不是x
- 参数
- 函数返回后,
x的值没有变化
关键知识点:指针参数本身也是值传递(传递地址的副本)。如果在函数内重新赋值指针(p = new(int)),只会修改副本的指向,不会影响调用者的原始指针。要修改原始值,应该使用 *p = 100 而不是 p = new(int)。
修正后的代码:
func modify(p *int) {
*p = 100 // 通过指针修改原始值
}
func main() {
x := 42
modify(&x)
fmt.Println(x) // 100
}