Go 编程实战:指针 Pointer——理解地址、取址与解引用

上一篇我们学习了 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

此时 p1p2 都保存 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

虽然 ab 的值都是 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

不过也不要为了使用指针而使用指针。对于 intbool 等简单数据,如果函数只需要读取:

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------参数、返回值与代码复用

相关推荐
To_OC9 小时前
从一行入口代码啃透 NestJS:工厂模式、装饰器和依赖注入是怎么串起来的
后端·设计模式·nestjs
Python私教10 小时前
多个项目怎么安全合并?先适配,再切换
后端·python·架构
黄敬峰10 小时前
告别 Vibe Coding!SDD 规范驱动开发实战:从文档到代码的 AI 开发新范式
面试
Python私教11 小时前
从表格到管理系统:别先写页面,先补齐权限、流程和审计
数据库·后端·架构
北斗落凡尘12 小时前
LangGraph 入门实战(12)--使用MCP
后端·python·langchain
嗯哼python12 小时前
从 0 到 1 构建 AI 模拟面试系统:RAG + LangGraph 的工程实践
人工智能·面试·职场和发展
ShineWinsu12 小时前
对于C++:布隆过滤器(bloomfilter)的详细解析
c++·面试·位图·位运算·布隆过滤器·海量数据·比特位
kyriewen13 小时前
我把 AI 写的并发请求控制器手写了一遍——3 个语义我当时根本讲不清
前端·javascript·面试
SomeB1oody13 小时前
【RustyML入门】6.0. 数学工具
开发语言·后端·机器学习·rust·教程