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