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

作者:程序员爱钓鱼日期:2026/8/21

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

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

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

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

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——理解地址、取址与解引用》 是转载文章,点击查看原文


相关推荐


【Linux系统篇】Linux入门基础指令(一)
忧郁兔斯基2026/8/8

在上一篇文章中,我们已经谈过Linux的历史以及如何使用云服务器配置Linux环境和使用Xshell来使用Linux,那么我们在本篇文章中将要开始学习Linux的一些基础指令。 目录 一、关于图形化界面 二、Linux文件以及目录结构 2.1 怎么理解文件 2.2 Linux的文件结构 2.3 Linux的目录结构 2.4 路径 三、简单的指令学习 3.1 pwd 命令 3.2 ls指令 -l 列出文件的详细信息 -a 列出目录下的所有文件 指令选项的组合使用方法 l


500 个服务时快 6 倍:我们如何将 Kibana APM 服务地图从 Canvas 重构到 React DOM
Elasticsearch2026/7/30

作者:来自 Elastic Jenny Pavlova 每个服务节点都会显示告警、 SLO 和异常健康状态,因此你可以仅筛选违反阈值的服务,并将结果嵌入到任何 Kibana 仪表板中,同时支持在整个拓扑中使用完整的键盘导航。 我们基于 React Flow 重构了 Elastic Observability 中的 Kibana APM 服务地图。在拥有 500 个服务时,渲染时间仅为 64ms,相比之前基于 Cytoscape.js 的实现快约 6 倍,同时 JavaScript 体积减少了


京东零售组织新变革:取消两级管理岗,推全栈化开发
AIHR数智引擎2026/7/22

近日,京东零售的组织调整方案流传出来。 很多人第一反应是:大厂又在砍中层。但把方案里的三条细节放在一起看,你会发现这事和"裁员"关系不大,它真正改写的,是"管理者"这三个字的定义。 细节一:C4、C5 两级管理者的"管理者身份"被取消。人还在,但"管人"这顶帽子没了,他们直接挂靠到 C3;以后员工请假、调休,C3 一个人拍板。 细节二:创新零售业务的前端团队并进服务端,全员转做全栈开发。第三季度开始,首批全栈工程师就要直接介入业务。 细节三:C2 序列的团队规模必须大于 50 人,没达标


AI编程出海第一步:别急着写代码,先找到老外真正愿意付费的需求
卷福同学2026/7/14

最近在学AI编程出海的项目,简单说就是做海外网站,让老外们付费使用。而第一步就是要确定网站做什么,也就是找需求 1.伪需求 对于小白来说,可能会想到和AI对话,聊出来一个需求,然后就开始做站了。但是这种AI直接生成的需求,只是看起来可以做,却没考虑到真实用户需求,往往做出来后没人用,或者已有成熟工具站了 2.真实的用户需求 以工具站为例,介绍2种方式来找真实的用户需求 Google Suggest 谷歌自动补齐,填写一个关键词到谷歌搜索,接着在前中后尝试输入a…z,会自动带出搜索词下拉列表,


协程深度解析:明明只有 8 个异步任务,为何 App 线程数瞬间突破 一倍?
潜龙勿用之化骨龙2026/7/6

在 Kotlin 协程中,有一个非常隐蔽但真实存在的问题: 你只是加了 Dispatchers.IO,线程数却变多了,多的不是一点点,而是成倍增长 更关键的是: 👉 这个问题在 Application 启动阶段,比 ViewModel 更严重 0. 先讲清楚“业务真实场景” 为了让问题更贴近真实工程,我们假设一个典型 App: App 启动 & UI 初始化行为 整个应用在启动时,会同时触发两类并发任务: ① Application 启动阶段(全局初始化) 启动 App → 自动


码农的AI翻身(三)你好,我叫 Embedding
Kfaino2026/6/28

AI翻身(三) 你好,我叫 Embedding——AI终于学会了理解,而不是死记硬背 大家好。 我叫 Embedding。 有人叫我: 向量。 有人叫我: 词向量。 还有人喜欢给我起一个特别高大上的名字: 语义空间映射。 听起来很厉害。 其实。 我就是一个翻译。 不过。 我翻译的不是中文和英文。 我翻译的是: 文字和数学。 我第一次见到老板的时候 老板(Transformer)对我说: "以后,人类说什么,你负责翻译。" 我愣住了。 "我不会中文啊。" 老板笑了。 "没关系。" "我也不会


笑抽了!DeepSeek识图,豆包完胜了!
甲维斯2026/6/19

听说 DeepSeek 识图功能上线了,我非常兴奋啊!终于要补上多模态这个短板了么? 打开APP和官网看了一眼: 真的出现了一个识图模式!哇塞! 我赶紧拿“梁爷爷”的图片试一波! 看到结果那一刻我瞬间,我忍不住笑出了声! 我的世界观被颠覆了。 原来这个人是腾讯公司高级副总裁、微信创始人张小龙。 我继续追问:那这个人是谁呢? 哇,世界观再次被颠覆!原来这两个人是同一个人?只是换了一个休息的造型而已??? 牛逼,还说出了 1、2、3、4,有理有据!好的,我信你了,这个人叫“张小龙”! 但是


Claude Code 每次调用 API 时,上下文是怎么"拼"出来的?
candyTong2026/6/11

Claude Code 每次调用模型 API 时,传给 API 的 payload 由三部分组成: System Prompt — 定义 Agent 的身份、行为规范和会话上下文 Tools — 工具 schema 列表,告诉模型有哪些能力可用 Messages — 对话消息,包含用户指令、CLAUDE.md 配置、工具执行结果 这三部分都会在 Agent Loop 调用模型时传入,但它们的来源不同:System Prompt 和 Tools 主要在进入循环前准备好,并在循环中保持相对稳定;


AI 降低了『写代码』的门槛,但是没有降低『软件开发』的复杂度
勇哥Java实战2026/6/4

好久没写文章了。 心里想写,却总觉得缺点由头。直到最近经历了几件事,彻底颠覆并重塑了我对 AI 编程的认知,骨鲠在喉,不吐不快。 我想聊聊那个被很多人忽略的真相:AI 确实拉低了「写代码」的门槛,但它并没有降低「软件开发」的复杂度。 1 售前朋友给我的惊喜 我曾经开源过一个项目——platform-sms。这是一个基于 SpringBoot 开发的短信网关服务,提供客户端 SDK,支持阿里云、腾讯云、亿美、合一等主流短信渠道,非常适合中小型公司。 这个项目最核心的设计亮点,在于我参考了阿里知名


CCFast 驰骋低代码BPM-积木菜单设计思想
驰骋低代码、工作流、表单引擎2026/5/28

CCFast 驰骋低代码 BPM:积木菜单设计思想  一、概述:为什么从“菜单”出发做低代码 1. CCFast 驰骋低代码 BPM 是一款开源的低代码开发与流程平台,面向企业信息化与业务流程数字化场景。 2. 本文章阐述:驰骋低代码 BPM 的整体体验与交付结构,根植于一套清晰、可扩展的菜单体系。 3. 其核心理念可以概括为:以菜单体系为骨架的低代码开发与运行平台——不是零散堆页面,而是用“可被授权、可被复用、可被组合”的菜单单元搭建系统。 4. 底座能力运行在组织结构管理与系统权限

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读