上一篇我们学习了 Map,到这里已经接触了数组、Slice、Map 等常用数据结构。在继续学习函数之前,还有一个非常重要的基础概念必须掌握——指针 Pointer。
很多刚接触 Go 的开发者看到指针会觉得复杂,其实 Go 的指针相比 C/C++ 简单很多。Go 不支持复杂的指针运算,日常开发主要掌握三个概念即可:变量、内存地址、通过地址访问变量。
理解指针之后,后面学习函数参数、Struct、方法、接口以及并发编程都会更加容易。
一、什么是指针
先看一个普通变量:
1age := 20 2
程序运行时,变量 age 的值需要存放在内存中的某个位置。
可以简单理解为:
1内存地址 数据 20x10001 -> 20 3
age 保存的数据是 20,而 0x10001 可以理解为这块数据所在的内存地址。
所谓指针,就是保存某个变量内存地址的变量。
例如:
1age := 20 2p := &age 3
这里:
1age -> 20 2p -> age的内存地址 3
因此 p 就是一个指针。
二、使用 & 获取变量地址
Go 使用:
1& 2
获取变量的地址。
例如:
1package main 2import "fmt" 3func main() { 4 age := 20 5 fmt.Println(age) 6 fmt.Println(&age) 7} 8
输出类似:
120 20xc0000120a0 3
具体地址每次运行可能不同,因此不要依赖某个固定地址。
这里:
1age 2
表示变量中的值,而:
1&age 2
表示变量的内存地址。
因此:
1p := &age 2
就是把 age 的地址保存到变量 p 中。
三、指针类型
普通整数变量:
1var age int 2
对应的整数指针类型:
1var p *int 2
这里的:
1*int 2
表示“指向 int 类型数据的指针”。
例如:
1age := 20 2var p *int 3p = &age 4
也可以简写:
1age := 20 2p := &age 3
Go 会自动推导 p 的类型为:
1*int 2
字符串指针:
1var p *string 2
布尔指针:
1var p *bool 2
如果有一个自定义类型:
1type User struct { 2 Name string 3} 4
对应的指针类型就是:
1*User 2
四、使用 * 获取指针指向的数据
如果:
1age := 20 2p := &age 3
此时 p 保存的是地址。
如果想通过 p 获取这个地址中的数据,需要使用:
1*p 2
例如:
1package main 2import "fmt" 3func main() { 4 age := 20 5 p := &age 6 fmt.Println(age) 7 fmt.Println(p) 8 fmt.Println(*p) 9} 10
输出类似:
120 20xc0000120a0 320 4
这里:
1p 2
得到地址。
而:
1*p 2
得到地址对应的数据。
这个操作通常叫做解引用。
五、& 和 * 的区别
这是学习指针最需要理解的两个符号。
1& 2
表示取地址。
1* 2
在表达式中表示通过指针访问它指向的数据。
例如:
1age := 20 2p := &age 3
可以理解为:
1age = 20 2&age = age的地址 3p = age的地址 4*p = 20 5
因此:
1p := &age 2fmt.Println(*p) 3
最终得到的仍然是 20。
六、通过指针修改变量
指针真正重要的地方,不只是读取数据,而是可以通过地址修改原变量。
例如:
1package main 2import "fmt" 3func main() { 4 age := 20 5 p := &age 6 *p = 30 7 fmt.Println(age) 8} 9
输出:
130 2
虽然没有直接执行:
1age = 30 2
但是:
1*p = 30 2
修改了 p 指向的内存,因此 age 也变成了 30。
可以理解为:
1age --------+ 2 | 3 v 4 [ 20 ] 5 ^ 6 | 7p ----------+ 8
执行:
1*p = 30 2
之后:
1age --------+ 2 | 3 v 4 [ 30 ] 5 ^ 6 | 7p ----------+ 8
变量和指针访问的是同一份数据。
七、为什么需要指针
假设定义一个函数:
1func change(age int) { 2 age = 30 3} 4
然后:
1func main() { 2 age := 20 3 change(age) 4 fmt.Println(age) 5} 6
结果仍然是:
120 2
因为 Go 的函数参数是值传递。
调用:
1change(age) 2
实际上会把 age 的值复制给函数参数。
可以理解为:
1main中的age = 20 2change中的age = 20 3
它们是两个不同的变量。
函数内部修改:
1age = 30 2
只修改函数内部的副本。
八、使用指针修改函数外变量
如果希望函数修改原变量,可以传递地址:
1package main 2import "fmt" 3func change(age *int) { 4 *age = 30 5} 6func main() { 7 age := 20 8 change(&age) 9 fmt.Println(age) 10} 11
输出:
130 2
调用:
1change(&age) 2
传入的是 age 的地址。
函数参数:
1age *int 2
接收到这个地址。
然后:
1*age = 30 2
通过地址修改原变量。
这是指针最典型的使用场景之一。
九、值传递和指针传递
普通参数:
1func change(value int) { 2 value = 100 3} 4
调用:
1num := 10 2change(num) 3
可以理解为:
1num -> 10 2 ↓复制 3value -> 10 4
修改 value 不影响 num。
如果使用指针:
1func change(value *int) { 2 *value = 100 3} 4
调用:
1change(&num) 2
可以理解为:
1num -> [10] 2 ↑ 3 | 4value --+ 5
此时函数可以修改 num。
严格来说,Go 中所有参数依然都是值传递。传递指针时,复制的是“地址值”,只是复制后的指针仍然指向同一份数据。
十、nil 指针
声明一个指针:
1var p *int 2
此时没有指向任何有效变量,因此:
1fmt.Println(p) 2
输出:
1<nil> 2
可以判断:
1if p == nil { 2 fmt.Println("空指针") 3} 4
需要特别注意,不能直接:
1fmt.Println(*p) 2
因为 p 没有指向有效数据,对 nil 指针进行解引用会导致运行时 panic。
因此处理可能为 nil 的指针时应该先判断:
1if p != nil { 2 fmt.Println(*p) 3} 4
十一、使用 new 创建指针
Go 提供内置函数:
1new() 2
例如:
1p := new(int) 2
它会为一个 int 类型的值分配空间,并返回指向该值的指针。
此时:
1fmt.Println(*p) 2
输出:
10 2
因为 int 的零值是 0。
可以修改:
1*p = 100 2fmt.Println(*p) 3
输出:
1100 2
因此:
1p := new(int) 2
得到的 p 类型是:
1*int 2
不过实际开发中,如果已经有变量,通常直接:
1p := &value 2
更加常见。
十二、指针之间赋值
例如:
1num := 10 2p1 := &num 3p2 := p1 4
此时 p1 和 p2 都保存 num 的地址。
因此:
1*p2 = 100 2
再执行:
1fmt.Println(num) 2fmt.Println(*p1) 3fmt.Println(*p2) 4
结果都是:
1100 2100 3100 4
因为三个操作最终访问的是同一份数据。
十三、指针可以比较
指针可以使用:
1== 2!= 3
进行比较。
例如:
1num := 10 2p1 := &num 3p2 := &num 4fmt.Println(p1 == p2) 5
输出:
1true 2
因为两个指针指向同一个变量。
例如:
1a := 10 2b := 10 3p1 := &a 4p2 := &b 5fmt.Println(p1 == p2) 6
通常得到:
1false 2
虽然 a 和 b 的值都是 10,但它们是不同变量,地址不同。
因此指针比较的是它们保存的地址是否相同,而不是指向的数据内容是否相等。
十四、Struct 与指针
指针在 Struct 中使用非常频繁。
例如:
1type User struct { 2 Name string 3 Age int 4} 5
创建:
1user := User{ 2 Name: "Tom", 3 Age: 20, 4} 5p := &user 6
此时 p 的类型是:
1*User 2
可以通过:
1(*p).Name 2
访问字段。
不过 Go 为 Struct 指针提供了语法简化,可以直接:
1p.Name 2
例如:
1p.Age = 30 2fmt.Println(user.Age) 3
输出:
130 2
因此实际 Go 项目中经常看到:
1func updateUser(user *User) { 2 user.Age = 30 3} 4
而不是:
1(*user).Age = 30 2
十五、为什么 Struct 经常使用指针
假设:
1type User struct { 2 Name string 3 Age int 4} 5
如果函数参数:
1func update(user User) { 2 user.Age = 30 3} 4
调用:
1user := User{Name: "Tom", Age: 20} 2update(user) 3
原来的 user 不会被修改。
如果:
1func update(user *User) { 2 user.Age = 30 3} 4
调用:
1update(&user) 2
就可以修改原对象。
另外,如果 Struct 非常大,直接传值需要复制整个 Struct,而传递指针只需要复制一个地址值。因此很多 Go 项目中的 Struct 方法会使用指针接收者:
1func (u *User) UpdateName(name string) { 2 u.Name = name 3} 4
后面学习方法时会详细讲解。
十六、Slice 需要传指针吗
上一篇我们看到:
1func change(nums []int) { 2 nums[0] = 100 3} 4
可以修改 Slice 中已有元素。
因此通常没有必要:
1func change(nums *[]int) 2
原因是 Slice 本身包含了对底层数组的引用信息。
不过如果函数需要直接修改调用方的 Slice 描述本身,例如希望函数内部 append 后让调用方获得新的 Slice,更常见的设计是返回新的 Slice:
1func add(nums []int, value int) []int { 2 return append(nums, value) 3} 4
调用:
1nums = add(nums, 100) 2
这种写法通常更加清晰。
十七、Map 需要传指针吗
Map 作为参数时:
1func change(m map[string]int) { 2 m["Tom"] = 100 3} 4
可以修改 Map 中的数据。
因此通常也没有必要写成:
1func change(m *map[string]int) 2
直接:
1func change(m map[string]int) 2
即可。
需要记住:Go 仍然是值传递,只不过 Map 的值内部包含运行时管理底层数据所需的信息,因此复制 Map 值后,仍然可以操作同一份底层 Map 数据。
十八、Go 不支持普通指针运算
在 C/C++ 中可能看到:
1p++ 2p + 1 3p - 1 4
使用指针直接计算内存地址。
普通 Go 指针不支持这种指针算术。
例如:
1p := &num 2p++ 3
是不允许的。
这使 Go 指针更加简单,也避免了大量直接操作任意内存地址产生的问题。
日常 Go 开发主要掌握:
1&value 2*pointer 3
基本已经可以应对绝大多数普通业务场景。
十九、指针实战:交换两个变量
假设:
1a := 10 2b := 20 3
希望通过函数交换两个变量。
如果:
1func swap(a int, b int) { 2 a, b = b, a 3} 4
调用后外部变量不会变化。
可以使用指针:
1package main 2import "fmt" 3func swap(a *int, b *int) { 4 *a, *b = *b, *a 5} 6func main() { 7 a := 10 8 b := 20 9 swap(&a, &b) 10 fmt.Println(a) 11 fmt.Println(b) 12} 13
输出:
120 210 3
这里:
1*a 2*b 3
直接操作两个原变量,因此可以完成交换。
二十、指针实战:修改用户信息
例如:
1package main 2import "fmt" 3type User struct { 4 Name string 5 Age int 6} 7func updateUser(user *User) { 8 user.Name = "Jack" 9 user.Age = 30 10} 11func main() { 12 user := User{ 13 Name: "Tom", 14 Age: 20, 15 } 16 updateUser(&user) 17 fmt.Println(user) 18} 19
输出:
1{Jack 30} 2
这就是 Go 项目中非常典型的指针使用方式。
二十一、什么时候使用指针
常见场景主要有几个。
第一,需要修改原变量:
1func update(value *int) 2
第二,需要修改 Struct:
1func updateUser(user *User) 2
第三,Struct 较大,不希望频繁复制:
1func process(data *BigStruct) 2
第四,需要使用 nil 表示“没有值”:
1var user *User = nil 2
不过也不要为了使用指针而使用指针。对于 int、bool 等简单数据,如果函数只需要读取:
1func printAge(age int) 2
通常直接传值更加简单。
二十二、常见错误
第一个错误是混淆 & 和 *:
1num := 10 2p := &num 3fmt.Println(*p) 4
记住:
1& -> 获取地址 2* -> 获取地址对应的数据 3
第二个错误是 nil 指针解引用:
1var p *int 2fmt.Println(*p) 3
会产生 panic。
第三个错误是认为传入指针就不是值传递。实际上:
1func test(p *int) 2
仍然是值传递,只不过复制的是地址值。
第四个错误是所有参数都使用指针。指针应该根据是否需要共享或修改数据等实际需求选择,而不是机械使用。
第五个错误是试图进行普通指针运算:
1p++ 2
Go 普通指针不支持这种操作。
二十三、总结
定义普通变量:
1num := 10 2
获取地址:
1p := &num 2
定义指针:
1var p *int 2
读取指针指向的数据:
1fmt.Println(*p) 2
修改:
1*p = 100 2
判断 nil:
1if p != nil { 2 fmt.Println(*p) 3} 4
函数接收指针:
1func change(num *int) { 2 *num = 100 3} 4
调用:
1change(&num) 2
学习 Pointer 最重要的是理解:
11.变量的数据存储在内存中 22.& 用于获取变量地址 33.* 用于通过指针访问对应数据 44.指针可以修改它指向的原变量 55.nil 指针不能直接解引用 66.Go 的函数参数始终是值传递 77.传递指针实际上也是复制地址值 88.Go 普通指针不支持指针算术 9
理解指针之后,我们就可以更加深入地学习 Go 中最基础也最重要的代码组织方式之一——函数。前面的示例已经大量使用了 main()、change()、swap() 等函数,但函数还有参数、返回值、多返回值、可变参数、匿名函数、闭包等非常重要的知识点。
下一篇:函数 Function——参数、返回值与代码复用
《Go 编程实战:指针 Pointer——理解地址、取址与解引用》 是转载文章,点击查看原文。