Golang 指针
一、先明确:什么是「内存地址」?
计算机的内存像无数个「小盒子」,每个盒子有唯一编号(即内存地址),用于存放数据。
- 变量本质是给某个「小盒子」起的名字,比如
a := 10
中,a
就是编号为0xc00001a0a8
(假设)的盒子的名字,盒子里装着10
。 - 「地址」就是这个盒子的编号(如
0xc00001a0a8
),「指针」就是专门用来存储这个编号的变量。
二、&
:取地址运算符——把变量的「盒子编号」取出来
&
的作用是获取变量对应的内存地址,返回值是一个「指针」(即地址本身)。
核心特性:
- 操作对象:必须是「变量」(不能对常量、表达式用
&
,比如&10
或&(a+b)
是错误的)。 - 返回结果:类型为「
*T
」(T
是原变量的类型),表示「指向T
类型的指针」。
示例:
package main
import "fmt"
func main() {
a := 10 // 变量a,存着10,地址假设为0xc00001a0a8
ptr := &a // 用&取a的地址,ptr就是指针(类型为*int)
fmt.Println(a) // 输出:10(a盒子里的内容)
fmt.Println(&a) // 输出:0xc00001a0a8(a的地址,即ptr的值)
fmt.Printf("ptr的类型:%T\n", ptr) // 输出:*int(指向int的指针)
}
类比理解:
- 你家地址是「北京市朝阳区XX街100号」(对应
&a
)。 - 你本人是
a
,&a
就是你家的门牌号。
三、*
:双重身份——定义指针类型 + 解引用指针
*
有两个完全不同的用法,需根据上下文区分:
1. 定义指针类型(声明变量时用)
在类型前加 *
,表示「这个变量是指针,专门存另一个变量的地址」。
格式:
var 指针变量名 *目标类型 // 声明一个指向「目标类型」的指针
示例:
var p *int // p是指针变量,只能存int类型变量的地址(或nil)
var s *string // s是指针变量,只能存string类型变量的地址(或nil)
注意:
- 未初始化的指针默认值为
nil
(表示不指向任何地址,类似「空门牌号」)。 - 指针类型严格匹配:
*int
不能存string
变量的地址,就像「只能存北京地址的本子,不能写上海地址」。
2. 解引用指针(使用指针时用)
对指针变量用 *
,表示「根据指针存的地址,找到对应的变量,获取/修改它的值」。
核心作用:
通过指针间接操作目标变量(不用直接用变量名)。
示例1:获取指针指向的值
a := 10
p := &a // p存a的地址(0xc00001a0a8)
fmt.Println(*p) // 输出:10(通过p的地址找到a,取出值)
示例2:通过指针修改目标变量的值
a := 10
p := &a
*p = 20 // 解引用p,找到a,把a的值改成20
fmt.Println(a) // 输出:20(a被修改)
类比理解:
p
是一张纸条,上面写着你家地址(&a
)。*p
就是「根据纸条上的地址找到你家,然后敲门看看里面有什么(取值),或者把里面的东西换了(修改值)」。
四、&
和 *
的「互逆关系」
对变量 x
来说:
&x
得到指针p
(p = &x
);*p
得到原变量x
(*p = x
)。
验证示例:
x := 5
p := &x // p = &x(p是x的地址)
fmt.Println(*p == x) // 输出:true(*p 等于 x的值)
注意:
&*p
等价于 p
(先解引用 p
得到 x
,再取 x
的地址,结果还是 p
);
*&x
等价于 x
(先取 x
的地址得到 p
,再解引用 p
,结果还是 x
)。
五、指针的实际用途:解决「值传递」的局限
Go 语言函数参数默认是「值传递」(传变量的副本),如果想在函数内修改外部变量,必须用指针。
反例:值传递无法修改外部变量
func modify(a int) {
a = 100 // 这里修改的是a的副本,和外部变量无关
}
func main() {
x := 10
modify(x)
fmt.Println(x) // 输出:10(x没被修改)
}
正例:用指针传递实现修改
func modify(p *int) {
*p = 100 // 解引用p,修改外部变量x的值
}
func main() {
x := 10
modify(&x) // 传x的地址(指针)
fmt.Println(x) // 输出:100(x被成功修改)
}
结构体场景:高效传递大对象
结构体可能包含大量字段(比如100个字段),值传递会复制整个结构体,效率低;而指针传递只复制一个地址(8字节,64位系统),更高效。
type Person struct {
name string
age int
// ... 很多字段
}
// 指针传递:只传地址,高效
func updateName(p *Person) {
p.name = "张三" // 直接修改原结构体
}
func main() {
p := Person{name: "李四", age: 20}
updateName(&p) // 传p的地址
fmt.Println(p.name) // 输出:张三(原结构体被修改)
}
六、new()
函数:另一种创建指针的方式
new(T)
会创建一个 T
类型的变量,初始化默认值(如 int
为0,string
为空),并返回该变量的指针(*T
类型)。
用法对比:
// 方法1:先定义变量,再取地址
var a int
p1 := &a // p1是*int类型,指向a(a的默认值为0)
// 方法2:用new()直接创建指针
p2 := new(int) // p2是*int类型,指向一个默认值为0的int变量
*p2 = 10 // 给指针指向的变量赋值
fmt.Println(*p1) // 输出:0
fmt.Println(*p2) // 输出:10
注意:
new()
只用于创建「基本类型」或「结构体」的指针,不能直接初始化复杂数据(如切片、映射,它们有专门的初始化函数)。
七、常见误区和注意事项
对 nil 指针解引用会崩溃
nil
指针不指向任何地址,对它用*
会触发运行时错误:var p *int // p是nil *p = 10 // 报错:panic: runtime error: invalid memory address or nil pointer dereference
指针也有自己的地址
指针本身也是变量,存储在内存中,所以&p
是「指针的地址」(类型为**int
,即「指向指针的指针」):a := 10 p := &a // p是*int类型,值为&a pp := &p // pp是**int类型,值为&p fmt.Println(*pp == p) // 输出:true(解引用pp得到p) fmt.Println(** pp == a) // 输出:true(连续解引用得到a)
不要过度使用指针
指针会增加代码复杂度,能不用就不用:- 基本类型(int、string等)传值效率很高,无需指针;
- 函数内不需要修改外部变量时,用值传递更清晰。
总结:核心公式
运算符 | 作用场景 | 示例 | 类比 |
---|---|---|---|
&x |
取变量x的地址,得指针 | p := &a |
问「a住在哪」,得到地址纸条p |
*p |
解引用指针p,得目标值 | fmt.Println(*p) |
按纸条p的地址找过去,看里面有什么 |
*p = v |
通过指针修改目标值 | *p = 20 |
按纸条p的地址找过去,把里面的东西换成v |
*T |
定义指向T类型的指针类型 | var p *int |
声明一个「只能写int类型地址」的纸条本 |
理解指针的关键:指针是「地址的载体」,&
负责生成这个载体,*
负责通过载体操作目标。多写几个修改变量、传递结构体的例子,很快就能熟练掌握~