Go 指针
指针是编程语言中用于存储变量内存地址的特殊类型。Go 提供了一定程度的指针支持,既保留了直接操作内存地址的能力,又通过安全机制(如没有指针运算、nil 检查)避免了 C 语言中指针的常见陷阱。本篇将详细讲解 Go 指针的概念和使用方法。
什么是指针
在计算机内存中,每个变量都存储在一个特定的地址上。指针就是存储这个地址的变量。
可以把内存想象成一排带编号的储物柜:
- 变量是柜子里存放的物品。
- 地址是柜子的编号。
- 指针就是一张写着柜子编号的纸条,通过它可以找到对应的物品。
package main
import "fmt"
func main() {
x := 42
fmt.Println("x 的值:", x) // 42
fmt.Println("x 的地址:", &x) // 0xc0000b4008(具体地址每次运行不同)
}& 取地址运算符
& 运算符用于获取变量的内存地址,结果是一个指针:
package main
import "fmt"
func main() {
name := "Go语言"
age := 15
pi := 3.14
fmt.Printf("name 的地址: %p\n", &name)
fmt.Printf("age 的地址: %p\n", &age)
fmt.Printf("pi 的地址: %p\n", &pi)
// 将地址保存到指针变量中
ptr := &age
fmt.Printf("ptr 的值(即 age 的地址): %p\n", ptr)
fmt.Printf("ptr 的类型: %T\n", ptr) // *int
}* 解引用运算符
* 运算符用于通过指针访问指针所指向的值,称为"解引用"或"间接引用":
package main
import "fmt"
func main() {
x := 100
ptr := &x // ptr 指向 x
// 通过指针读取值
fmt.Println("ptr 指向的值:", *ptr) // 100
// 通过指针修改值
*ptr = 200
fmt.Println("修改后 x 的值:", x) // 200
}指针类型
Go 中指针的类型用 *T 表示,其中 T 是指针指向的类型:
package main
import "fmt"
func main() {
var a int = 10
var b float64 = 3.14
var c string = "Hello"
var pa *int = &a
var pb *float64 = &b
var pc *string = &c
fmt.Printf("pa 类型: %T, 值: %v\n", pa, *pa) // *int, 10
fmt.Printf("pb 类型: %T, 值: %v\n", pb, *pb) // *float64, 3.14
fmt.Printf("pc 类型: %T, 值: %v\n", pc, *pc) // *string, Hello
}注意:不同类型的指针之间不能互相赋值,
*int和*float64是完全不同的类型。
值传递 vs 引用传递
值传递(默认行为)
Go 中函数参数默认是值传递,即传递的是值的副本,修改参数不会影响原始变量:
package main
import "fmt"
func increment(n int) {
n++ // 修改的是副本
fmt.Println("函数内 n:", n)
}
func main() {
x := 10
increment(x)
fmt.Println("main 中 x:", x) // 仍然是 10
}使用指针实现"引用传递"
通过传递指针,函数可以修改原始变量:
package main
import "fmt"
func increment(n *int) {
*n++ // 修改指针指向的值
fmt.Println("函数内 *n:", *n)
}
func main() {
x := 10
increment(&x) // 传递 x 的地址
fmt.Println("main 中 x:", x) // 11
}对比示例
package main
import "fmt"
// 值传递:交换失败
func swapByValue(a, b int) {
a, b = b, a
fmt.Printf("swapByValue 内: a=%d, b=%d\n", a, b)
}
// 指针传递:交换成功
func swapByPointer(a, b *int) {
*a, *b = *b, *a
fmt.Printf("swapByPointer 内: *a=%d, *b=%d\n", *a, *b)
}
func main() {
x, y := 1, 2
swapByValue(x, y)
fmt.Printf("swapByValue 后: x=%d, y=%d\n", x, y) // 1, 2(未变)
swapByPointer(&x, &y)
fmt.Printf("swapByPointer 后: x=%d, y=%d\n", x, y) // 2, 1(已交换)
}指针作为函数参数
指针参数在实际开发中非常常见,主要用于以下场景:
避免大结构体的拷贝
package main
import "fmt"
type User struct {
Name string
Age int
Email string
}
// 使用指针避免拷贝整个结构体
func printUser(u *User) {
fmt.Printf("姓名: %s, 年龄: %d, 邮箱: %s\n", u.Name, u.Age, u.Email)
}
func main() {
user := User{
Name: "张三",
Age: 25,
Email: "zhangsan@example.com",
}
printUser(&user)
}修改函数外部的数据
package main
import "fmt"
func setName(u *User, name string) {
u.Name = name // 等价于 (*u).Name = name
}
func main() {
user := User{Name: "张三", Age: 25}
setName(&user, "李四")
fmt.Println("修改后的名字:", user.Name) // 李四
}new 函数
new(T) 分配一个 T 类型的零值内存,并返回指向它的指针:
package main
import "fmt"
func main() {
// new(int) 分配一个 int 的内存,初始化为 0,返回 *int
p := new(int)
fmt.Printf("类型: %T, 值: %d\n", p, *p) // *int, 0
*p = 42
fmt.Println("赋值后:", *p) // 42
// 等价于:
// var temp int // 零值 0
// p := &temp
}提示:从 Go 1.18 开始,可以使用泛型辅助函数
func Ptr[T any](v T) *T { return &v }来创建任意类型的指针。
nil 指针
零值指针的值为 nil,对 nil 指针解引用会导致程序崩溃(panic):
package main
import "fmt"
func main() {
var p *int // 零值为 nil
fmt.Println("p == nil:", p == nil) // true
// fmt.Println(*p) // panic: runtime error: invalid memory address
// 正确做法:解引用前先检查
if p != nil {
fmt.Println(*p)
} else {
fmt.Println("指针为空,无法解引用")
}
}指针的常见使用场景
场景一:结构体方法接收者
当方法需要修改结构体的字段时,使用指针接收者:
package main
import "fmt"
type Counter struct {
count int
}
// 值接收者:不能修改原始结构体
func (c Counter) GetCount() int {
return c.count
}
// 指针接收者:可以修改原始结构体
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Reset() {
c.count = 0
}
func main() {
counter := Counter{}
counter.Increment()
counter.Increment()
counter.Increment()
fmt.Println("计数:", counter.GetCount()) // 3
counter.Reset()
fmt.Println("重置后:", counter.GetCount()) // 0
}场景二:链表节点
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func main() {
// 创建链表: 1 -> 2 -> 3
head := &Node{Value: 1}
head.Next = &Node{Value: 2}
head.Next.Next = &Node{Value: 3}
// 遍历链表
current := head
for current != nil {
fmt.Printf("%d -> ", current.Value)
current = current.Next
}
fmt.Println("nil")
}场景三:可选字段
在结构体中,指针类型字段可以表示"有值"或"无值"(nil):
package main
import "fmt"
type Profile struct {
Name string
Age int
Website *string // 可选字段,nil 表示未填写
}
func main() {
// 没有网站
p1 := Profile{Name: "张三", Age: 25}
printProfile(p1)
// 有网站
site := "https://example.com"
p2 := Profile{Name: "李四", Age: 30, Website: &site}
printProfile(p2)
}
func printProfile(p Profile) {
fmt.Printf("姓名: %s, 年龄: %d", p.Name, p.Age)
if p.Website != nil {
fmt.Printf(", 网站: %s", *p.Website)
}
fmt.Println()
}总结
本篇详细讲解了 Go 指针的核心概念和使用方法。指针存储变量的内存地址,& 取地址、* 解引用是两个基本操作。Go 的值传递默认传递副本,通过指针可以实现"引用传递"效果。new 函数用于分配零值内存,nil 指针需要在解引用前检查。在实际开发中,指针常用于:避免大结构体拷贝、修改函数外部数据、结构体方法接收者、链表等数据结构、以及表示可选字段。Go 的指针设计在灵活性和安全性之间取得了良好的平衡。