【Go语言-Day 9】指针基础:深入理解内存地址与值传递
Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
Python系列文章目录
Go语言系列文章目录
01-【Go语言-Day 1】扬帆起航:从零到一,精通 Go 语言环境搭建与首个程序
02-【Go语言-Day 2】代码的基石:深入解析Go变量(var, :=)与常量(const, iota)
03-【Go语言-Day 3】从零掌握 Go 基本数据类型:string
, rune
和 strconv
的实战技巧
04-【Go语言-Day 4】掌握标准 I/O:fmt 包 Print, Scan, Printf 核心用法详解
05-【Go语言-Day 5】掌握Go的运算脉络:算术、逻辑到位的全方位指南
06-【Go语言-Day 6】掌控代码流:if-else 条件判断的四种核心用法
07-【Go语言-Day 7】循环控制全解析:从 for 基础到 for-range 遍历与高级控制
08-【Go语言-Day 8】告别冗长if-else:深入解析 switch-case 的优雅之道
09-【Go语言-Day 9】指针基础:深入理解内存地址与值传递
文章目录
- Langchain系列文章目录
- Python系列文章目录
- PyTorch系列文章目录
- 机器学习系列文章目录
- 深度学习系列文章目录
- Java系列文章目录
- JavaScript系列文章目录
- Python系列文章目录
- Go语言系列文章目录
- 前言
- 一、初识内存与地址
- 1.1 计算机内存的奥秘
- 1.2 如何查看变量的地址
- 二、指针的核心:声明与操作
- 2.1 指针变量的声明
- 2.2 指针的“双雄”:`&` 与 `*`
- 2.2.1 取地址运算符 `&`
- 2.2.2 取值运算符 `*` (解引用)
- 2.3 空指针 `nil`
- 三、为何要拥抱指针:值传递 vs 引用传递
- 3.1 Go语言的默认机制:值传递
- 3.2 指针的威力:实现引用传递的效果
- 3.3 指针的应用场景总结
- 四、总结
前言
大家好!经过前面几篇文章对 if
、for
、switch
等流程控制语句的学习,我们已经能够构建出逻辑丰富的程序了。从今天开始,我们将踏入 Go 语言中一个极为核心且强大的领域——指针(Pointer)。许多初学者闻“指针”色变,认为它复杂难懂。但实际上,Go 语言的指针相比 C/C++ 已经做了很多简化和安全限制,理解它将为你打开一扇通往更高阶编程世界的大门。本篇作为指针系列的上篇,将拨开云雾,带你从最基础的内存地址概念入手,彻底搞懂指针是什么、如何使用以及为何要使用它。
一、初识内存与地址
在深入指针之前,我们必须先建立一个清晰的“心理模型”——程序中的数据是如何存储的。
1.1 计算机内存的奥秘
想象一下,计算机的内存就像一个巨大无比的酒店,拥有成千上万个房间,每个房间都有一个独一无二的门牌号。
- 数据 (Data):就像是住进酒店的客人。你在程序中定义的每一个变量(无论是数字、字符串还是其他类型),都像一位客人,需要一个地方住。
- 内存单元 (Memory Cell):就是酒店里的房间,用来存放数据。
- 内存地址 (Memory Address):就是每个房间的门牌号。通过这个地址,CPU 可以快速、准确地找到存储特定数据的那个内存单元。
简单来说,当你在 Go 中声明一个变量时,计算机会在内存中为它分配一个“房间”,并将变量的值存进去,这个“房间”的门牌号就是它的内存地址。
1.2 如何查看变量的地址
Go 语言提供了一个非常直观的操作符 &
(取地址符),它可以获取任意变量的内存地址。
我们来看一个简单的例子:
package mainimport "fmt"func main() {// 声明一个整型变量 avar a int = 10// 使用 &a 获取变量 a 的内存地址// 使用 fmt.Printf 和 %p 占位符来打印地址fmt.Printf("变量 a 的值是: %d\n", a)fmt.Printf("变量 a 的内存地址是: %p\n", &a)
}
代码解析:
var a int = 10
:我们在内存中开辟了一块空间,命名为a
,并存入了整数10
。&a
:这里的&
就像一个探测器,它不关心a
里面存的是什么,只关心a
这个“房间”的门牌号是多少。%p
:这是fmt.Printf
中专门用来格式化输出内存地址的占位符(p for pointer)。
运行上述代码,你可能会看到类似这样的输出(地址每次运行都可能不同):
变量 a 的值是: 10
变量 a 的内存地址是: 0xc000018090
这个 0xc000018090
就是变量 a
在本次程序运行时,操作系统分配给它的独一无二的内存地址。
二、指针的核心:声明与操作
理解了内存地址,指针的概念就水到渠成了。
核心定义:指针(Pointer)就是一个特殊类型的变量,它不存储普通的数据值,而是专门用来存储另一个变量的内存地址。
如果说普通变量是直接存放“货物”的仓库,那么指针变量就是一张记录着“某个货物存放在哪个仓库”的地图。
2.1 指针变量的声明
Go 语言中指针变量的声明格式为:
var ptr *T
ptr
:是你给指针变量起的名字。*
:星号在这里表明我们正在声明的是一个指针变量。T
:代表该指针要指向的变量的类型。例如,一个指向int
类型变量的指针,其类型就是*int
;一个指向string
类型变量的指针,其类型就是*string
。
示例:
package mainimport "fmt"func main() {var a int = 100// 声明一个可以指向 int 类型的指针变量 pvar p *int// 将变量 a 的地址赋值给指针 pp = &afmt.Printf("变量 a 的地址是: %p\n", &a)fmt.Printf("指针变量 p 中存储的值是: %p\n", p)fmt.Printf("指针变量 p 自身的地址是: %p\n", &p)
}
代码解析:
var p *int
:声明了一个名为p
的指针变量,它的类型是*int
,意味着它只能存储int
类型变量的地址。p = &a
:我们将a
的地址 (&a
) 赋值给了p
。现在,p
就“指向”了a
。- 从输出可以看到,
a
的地址和p
中存储的值是完全一样的。同时,p
本身也是一个变量,它也有自己独立的内存地址。
2.2 指针的“双雄”:&
与 *
掌握指针,关键在于熟练运用一对核心操作符:&
和 *
。
2.2.1 取地址运算符 &
这个我们已经很熟悉了,它的作用是获取一个变量的内存地址。
2.2.2 取值运算符 *
(解引用)
当 *
用在指针变量前面时,它的含义就变成了“取值”或“解引用”(Dereference)。它的作用是:通过指针中存储的地址,找到该地址对应的内存空间,并获取其中存储的真正的值。
综合示例:
让我们通过一个完整的例子来看看如何通过指针读取并修改原始变量的值。
package mainimport "fmt"func main() {var num int = 42var ptr *int // 声明一个 int 指针ptr = &num // ptr 指向 numfmt.Printf("原始变量 num 的值: %d\n", num)fmt.Printf("通过指针 ptr 获取的值: %d\n", *ptr) // 使用 *ptr 解引用// 现在,通过指针修改值*ptr = 100fmt.Println("--- 通过指针修改值后 ---")fmt.Printf("指针 ptr 指向的地址没变: %p\n", ptr)fmt.Printf("现在,原始变量 num 的值变成了: %d\n", num)fmt.Printf("通过指针 ptr 再次获取的值: %d\n", *ptr)
}
代码解析与关键点:
ptr = &num
:指针ptr
拿到了num
的“门牌号”。*ptr
:当我们使用*ptr
时,Go 语言会:- 查看
ptr
变量里存的地址(比如是0xc000018090
)。 - 找到内存中
0xc000018090
这个位置。 - 读取或操作这个位置上的值。
- 查看
*ptr = 100
:这是最关键的一步!这行代码的意思是:“找到ptr
指向的那个内存地址,把那个地址上的值给我改成100
”。由于ptr
指向的是num
,所以实际上被修改的就是num
变量本身。
输出:
原始变量 num 的值: 42
通过指针 ptr 获取的值: 42
--- 通过指针修改值后 ---
指针 ptr 指向的地址没变: 0xc0000b2008
现在,原始变量 num 的值变成了: 100
通过指针 ptr 再次获取的值: 100
这个例子完美地展示了指针的威力:它允许我们在程序的某个地方,间接地修改另一个地方的变量值。
2.3 空指针 nil
当我们声明一个指针变量但没有给它赋任何地址时,它的默认值是 nil
。
nil
是 Go 语言中指针、接口、map、切片、channel 和函数类型的零值。对于指针来说,nil
指针表示它不指向任何有效的内存地址。
package mainimport "fmt"func main() {var p *intfmt.Printf("指针 p 的值: %v\n", p) // 输出 <nil>// 重要的安全检查if p != nil {fmt.Println("p 指向的值是:", *p)} else {fmt.Println("p 是一个空指针,不能解引用!")}// 下面的代码会引发 panic// fmt.Println(*p) // invalid memory address or nil pointer dereference
}
重要警告:对一个 nil
指针进行解引用(取值 *p
)是一个严重的运行时错误,会导致程序崩溃(panic)。因此,在使用指针前,进行 nil
检查是一个非常重要的好习惯。
三、为何要拥抱指针:值传递 vs 引用传递
现在你可能会问,我直接用变量 num
不就好了,为什么要费劲去用指针 ptr
呢?答案在于理解 Go 函数的传参机制。
3.1 Go语言的默认机制:值传递
在 Go 语言中,所有的函数参数传递都是值传递(Pass by Value)。这意味着当你将一个变量作为参数传递给函数时,函数接收到的是这个变量的一个副本(Copy)。函数内部对这个副本的任何修改,都不会影响到函数外部的原始变量。
示例:
package mainimport "fmt"func modifyValue(val int) {fmt.Printf("进入函数,val 的地址: %p\n", &val)val = 200 // 修改的是副本的值
}func main() {num := 100fmt.Printf("调用前,num 的值: %d, 地址: %p\n", num, &num)modifyValue(num) // 将 num 的值 "100" 传递给函数fmt.Printf("调用后,num 的值: %d, 地址: %p\n", num, &num)
}
输出:
调用前,num 的值: 100, 地址: 0xc000018098
进入函数,val 的地址: 0xc0000180c0
调用后,num 的值: 100, 地址: 0xc000018098
分析:
main
函数中的num
和modifyValue
函数中的val
拥有完全不同的内存地址。- 调用
modifyValue(num)
时,Go 只是把num
的值100
复制了一份给了val
。 - 函数内部
val = 200
修改的仅仅是val
这个副本,和main
函数中的num
毫无关系。
3.2 指针的威力:实现引用传递的效果
如果我们想让一个函数能够修改外部变量的值,就需要把这个变量的地址(也就是指针)传递给函数。
示例:
package mainimport "fmt"func modifyByPointer(ptr *int) { // 参数是一个 *int 类型的指针fmt.Printf("进入函数,ptr 存储的地址: %p\n", ptr)*ptr = 200 // 通过指针解引用,修改原始地址上的值
}func main() {num := 100fmt.Printf("调用前,num 的值: %d, 地址: %p\n", num, &num)modifyByPointer(&num) // 将 num 的地址传递给函数fmt.Printf("调用后,num 的值: %d, 地址: %p\n", num, &num)
}
输出:
调用前,num 的值: 100, 地址: 0xc000018098
进入函数,ptr 存储的地址: 0xc000018098
调用后,num 的值: 200, 地址: 0xc000018098
分析:
- 这次我们传递的是
&num
,即num
的内存地址。 - 函数
modifyByPointer
的参数ptr
接收了这个地址。现在ptr
和num
指向了同一块内存。 - 函数内部执行
*ptr = 200
,就直接修改了那块内存中的值,因此main
函数中的num
也随之改变。
虽然这本质上仍然是值传递(传递的是地址这个值),但它达到了引用传递(Pass by Reference) 的效果。
3.3 指针的应用场景总结
通过上面的对比,我们可以清晰地看到指针的第一个核心用途:
- 在函数间共享数据:当你想让一个函数能够修改调用方的数据时,传递指针是必由之路。
- 提高性能:对于非常大的数据结构(例如一个包含很多字段的复杂
struct
),如果每次函数调用都完整地复制一份,开销会非常大。而传递一个指针(通常只有 8 个字节)则非常轻量高效。这一点我们将在后续的结构体学习中深入体会。
四、总结
恭喜你,完成了 Go 指针基础的学习!让我们回顾一下今天的核心知识点:
- 内存与地址:程序中的每个变量都存储在内存的特定位置,这个位置由其内存地址唯一标识。
- 指针定义:指针是一种特殊的变量,它存储的是另一个变量的内存地址。
- 核心操作符:
&
(取地址符):获取变量的内存地址。*
(解引用/取值符):当用在指针前时,获取指针所指向地址上的值。
- 空指针
nil
:指针变量的零值是nil
,表示不指向任何地方。对nil
指针解引用会导致程序崩溃,使用前应做检查。 - 指针的核心价值:Go 默认是值传递,函数无法修改外部变量。通过传递指针,可以让函数间接地操作和修改原始数据,实现引用传递的效果,这对于数据共享和性能优化至关重要。
指针是理解 Go 语言许多高级特性的基石,如下一篇将要讲到的 new
和 make
的区别,以及后续的切片、map、结构体方法的实现都与它息息相关。请务必动手实践本文中的所有代码,加深理解。在下一篇文章 【Go语言-Day 10】指针 - Go 语言的利器 (下) 中,我们将继续探索指针在函数、数组、结构体中的高级应用,以及 new
与 make
的辨析,敬请期待!