Go 入门到精通-13-指针与内存
目录
- 🔵 Go 入门到精通:指针与内存
- 13.1 指针是什么
- 13.2 取地址与解引用
- 13.2.1 `&` 取地址运算符
- 13.2.2 `*` 解引用运算符
- 13.2.3 完整示例
- 13.3 new 函数:分配零值返回指针
- 13.3.1 new 的使用场景
- 13.3.2 new 很少单独使用
- 13.4 make 与 new 的区别
- 13.4.1 核心区别
- 13.4.2 为什么需要 make
- 13.4.3 速查表
- 13.5 值传递 vs 指针传递
- 13.5.1 值传递的代价
- 13.5.2 选择决策表
- 13.5.3 基准测试参考
- 13.6 指针的指针
- 13.6.1 实际应用:JSON反序列化
- 13.7 指针与结构体方法
- 13.7.1 语法回顾
- 13.7.2 编译器自动转换
- 13.7.3 接收者选择速查
- 13.8 unsafe.Pointer 简介
- 13.8.1 指针类型层次
- 13.8.2 基本用法示例
- 13.8.3 unsafe 的合法使用场景
- 13.9 逃逸分析
- 13.9.1 什么是逃逸
- 13.9.2 查看逃逸分析
- 13.9.3 常见逃逸场景
- 13.9.4 逃逸分析的意义
- 13.10 栈与堆:分配决策
- 13.10.1 栈 vs 堆对比
- 13.10.2 Go的分配策略
- 13.10.3 减少堆分配的技巧
- 13.11 指针使用最佳实践
- 13.11.1 原则总结
- 13.11.2 原则速查
- 13.12 综合案例:内存高效缓存
- 13.13 本章总结
🔵 Go 入门到精通:指针与内存
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处
🧑💻 作者:布朗克168 | 系列:Go入门到精通 2026 · 第三阶段 · 核心进阶
13.1 指针是什么
指针是存储另一个变量内存地址的变量。通俗地说,指针告诉你"某个值在哪里"而非"那个值是什么"。
x:=42p:=&x// p 是指向 x 的指针,存储了 x 的地址fmt.Println(x)// 42fmt.Println(p)// 0xc0000140a0(某个内存地址)fmt.Println(*p)// 42(通过指针读取 x 的值)内存模型图:
变量 x (地址: 0xc0000140a0) ┌─────────────────────┐ │ 42 │ ← x 的值 └─────────────────────┘ ↑ │ 指针 p 存储了这个地址 │ 变量 p ┌─────────────────────┐ │ 0xc0000140a0 │ ← p 的值 = x 的地址 └─────────────────────┘| 概念 | 说明 | 类比 |
|---|---|---|
| 值 | 直接存储的数据 | 你的家 |
| 地址 | 数据存储的位置 | 你家的门牌号 |
| 指针 | 存储地址的变量 | 记着门牌号的纸条 |
| 解引用 | 通过地址访问数据 | 按门牌号找到你的家 |
13.2 取地址与解引用
13.2.1&取地址运算符
varxint=100p:=&x// & 获取 x 的地址,p 是 *int 类型fmt.Printf("x 的值: %d\n",x)fmt.Printf("x 的地址: %p\n",&x)fmt.Printf("p 的值(即x的地址): %p\n",p)fmt.Printf("p 的类型: %T\n",p)// *int13.2.2*解引用运算符
*p=200// 通过指针修改 x 的值fmt.Println(x)// 200(x 被修改了!)fmt.Println(*p)// 200// * 的双重身份:// 1. 在类型声明中:*int 表示"指向int的指针"类型// 2. 在表达式中:*p 表示"p所指向的值"(解引用)13.2.3 完整示例
funcswap(a,b*int){*a,*b=*b,*a// 通过指针交换值}funcmain(){x,y:=10,20swap(&x,&y)fmt.Println(x,y)// 20, 10 ✅}13.3 new 函数:分配零值返回指针
new(T)是Go内置函数,用于分配内存:
p:=new(int)// p 是 *int 类型fmt.Println(*p)// 0(零值)*p=42fmt.Println(*p)// 42// new(T) 等价于以下两步:varv T p:=&v13.3.1 new 的使用场景
// 创建结构体指针user:=new(User)// 等同 &User{}user.Name="张三"// 创建基本类型指针(较少用)flag:=new(bool)*flag=true13.3.2 new 很少单独使用
| 方式 | 代码 | 使用频率 |
|---|---|---|
new | u := new(User) | ⭐ 较少 |
| 字面量取地址 | u := &User{} | ⭐⭐⭐ 最常见 |
| 字面量+字段 | u := &User{Name: "张三"} | ⭐⭐⭐⭐⭐ 最推荐 |
实际上,Go社区更偏好&T{}而非new(T),因为前者可以同时初始化字段。
13.4 make 与 new 的区别
这是Go初学者必问的问题。两者都分配内存,但用途截然不同:
13.4.1 核心区别
| 维度 | new(T) | make(T, ...) |
|---|---|---|
| 适用类型 | 任意类型 | 仅slice、map、chan |
| 返回值 | *T(指针) | T(值本身,非指针) |
| 初始化 | 填充零值 | 填充内部数据结构 |
| 能否直接用 | ✅ 可以 | ✅ 可以 |
13.4.2 为什么需要 make
slice、map、chan这三种类型在底层是指向数据结构的指针,使用前必须初始化其内部结构:
// ❌ nil map 不能写入varmmap[string]intm["key"]=1// panic: assignment to entry in nil map// ✅ 用 make 初始化m=make(map[string]int)m["key"]=1// OK// ❌ nil slice 不能追加(其实可以,但不推荐)vars[]ints=append(s,1)// 实际上可以,但容量为0// ✅ 用 make 预分配s=make([]int,0,10)// 长度0,容量1013.4.3 速查表
// new:返回指针p:=new(int)// *int, *p = 0u:=new(User)// *User, u.Name = ""// make:返回值s:=make([]int,5,10)// []int, len=5, cap=10m:=make(map[string]int)// map[string]intch:=make(chanint,10)// chan int, 缓冲区大小10🔑记忆口诀:“new给所有类型分配零值指针,make只为三兄弟(slice/map/chan)初始化。”
13.5 值传递 vs 指针传递
Go中所有函数参数都是值传递——即使传指针,也是传递了指针值的副本。
13.5.1 值传递的代价
typeBigStructstruct{Data[1024*1024]byte// 1MB}funcprocessValue(bs BigStruct){// ❌ 传递时拷贝 1MB// ...}funcprocessPointer(bs*BigStruct){// ✅ 只传递 8 字节指针// ...}13.5.2 选择决策表
| 场景 | 推荐 | 原因 |
|---|---|---|
| 需要修改原值 | 指针 | 值传递无法修改 |
| 结构体 > 64字节 | 指针 | 避免大内存拷贝 |
| 结构体含Mutex等不可复制字段 | 指针 | 防止意外复制锁 |
| 小型不可变结构体 | 值 | 更安全、无nil风险 |
| 基本类型(int, float64等) | 值 | 拷贝成本极低(8字节) |
| 需要并发安全隔离 | 值 | 每个goroutine独立副本 |
13.5.3 基准测试参考
// 小结构体(24字节)typeSmallstruct{a,b,cint64}// 大结构体(~1KB)typeLargestruct{data[128]int64}| 结构体大小 | 值传递 | 指针传递 | 建议 |
|---|---|---|---|
| ≤64字节 | 更快 | 略慢(需解引用) | 用值 |
| >64字节 | 慢(大量拷贝) | 快 | 用指针 |
| 含Mutex | 编译报错/运行时竞态 | 正确 | 必须用指针 |
⚠️注意:
sync.Mutex等同步原语不能复制(go vet会警告),包含它们的结构体必须用指针传递。
13.6 指针的指针
指针本身也是值,因此可以有指向指针的指针:
x:=42p:=&x// *int, 指向 xpp:=&p// **int, 指向 p(指针的指针)fmt.Println(x)// 42fmt.Println(*p)// 42fmt.Println(**pp)// 42(两次解引用)**pp=100fmt.Println(x)// 100 ✅内存布局:
x (42) ← p (存储&x) ← pp (存储&p) [ 42 ] [ &x ] [ &p ] *p = 42 *pp = &x **pp = 4213.6.1 实际应用:JSON反序列化
varconfig*ServerConfig json.Unmarshal(data,&config)// 需要传 **ServerConfig// ↑ 因为 config 本身就是指针// &config 是指针的指针13.7 指针与结构体方法
这在第11章已经详细讨论过,此处做系统总结:
13.7.1 语法回顾
typeCounterstruct{countint}// 指针接收者:能修改原值func(c*Counter)Increment(){c.count++}// 值接收者:不能修改原值func(c Counter)Count()int{returnc.count}13.7.2 编译器自动转换
c:=Counter{count:0}c.Increment()// ✅ Go自动转换为 (&c).Increment()cp:=&Counter{count:0}cp.Count()// ✅ Go自动转换为 (*cp).Count()13.7.3 接收者选择速查
| 需要修改原值? | 结构体很大? | 包含不可复制字段? | 选择 |
|---|---|---|---|
| ✅ | - | - | 指针 |
| ❌ | ✅ | - | 指针 |
| ❌ | ❌ | ✅ | 指针 |
| ❌ | ❌ | ❌ | 值 |
13.8 unsafe.Pointer 简介
⚠️声明:
unsafe包如其名——不安全。本节仅为认知性介绍,生产代码中应尽量避免使用。
13.8.1 指针类型层次
普通指针 (*T) → 类型安全,编译器检查 ↓ 可转换 unsafe.Pointer → 通用指针类型,可以与其他指针互转 ↓ 可转换 uintptr → 整数类型,可做算术运算13.8.2 基本用法示例
import"unsafe"// 在不同类型的指针间转换(极其危险!)varffloat64=3.14p:=unsafe.Pointer(&f)// *float64 → unsafe.Pointerip:=(*int64)(p)// unsafe.Pointer → *int64fmt.Println(*ip)// 浮点数的底层bit表示// 访问结构体未导出字段(绕过封装)typeUserstruct{namestring// 小写,外部不可见ageint}u:=User{name:"secret",age:25}namePtr:=(*string)(unsafe.Pointer(&u))fmt.Println(*namePtr)// "secret" ← 绕过了封装!13.8.3 unsafe 的合法使用场景
| 场景 | 说明 |
|---|---|
| 系统调用 | syscall包内部大量使用 |
| 与C互操作 | cgo中桥接C指针 |
| 高性能序列化 | 直接操作内存布局 |
| 零拷贝转换 | string↔[]byte转换(需审慎) |
🔴警告:普通业务代码中遇到
unsafe应该警觉,它绕过了Go所有的内存安全保护。
13.9 逃逸分析
逃逸分析(Escape Analysis)是Go编译器的一项关键优化,它决定一个变量分配在栈上还是堆上。
13.9.1 什么是逃逸
// 不逃逸:分配在栈上funcstayOnStack()int{x:=42returnx// x的生命周期在函数内,分配在栈上}// 逃逸:分配在堆上funcescapeToHeap()*int{x:=42return&x// x 的地址被返回,x "逃逸"到堆上}13.9.2 查看逃逸分析
go build-gcflags="-m"main.go# 或更详细go build-gcflags="-m -m"main.go输出示例:
./main.go:5:6: can inline stayOnStack ./main.go:10:6: can inline escapeToHeap ./main.go:12:9: &x escapes to heap ./main.go:11:6: moved to heap: x13.9.3 常见逃逸场景
| 场景 | 是否逃逸 | 示例 |
|---|---|---|
| 返回局部变量指针 | ✅ 逃逸 | return &x |
| 将指针存入全局变量 | ✅ 逃逸 | global = &x |
| 将指针发送到channel | ✅ 逃逸 | ch <- &x |
| 将指针存入切片/Map | ✅ 逃逸 | m["key"] = &x |
| 局部变量未暴露引用 | ❌ 不逃逸 | return x |
| 接口动态调用 | 可能逃逸 | fmt.Println(x) |
| 闭包引用外部变量 | ✅ 逃逸 | func() { use(&x) } |
13.9.4 逃逸分析的意义
栈分配:速度快,函数返回后自动回收 堆分配:需GC回收,有额外开销 编译器尽力将变量分配在栈上,减少GC压力💡核心思想:不要过早优化!先写清晰的代码,然后用
-gcflags="-m"分析,只优化真正的热点路径。
13.10 栈与堆:分配决策
13.10.1 栈 vs 堆对比
| 维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配速度 | 极快(移动栈指针) | 较慢(需查找空闲块) |
| 回收方式 | 函数返回时自动回收 | GC扫描回收 |
| 大小限制 | 较小(通常MB级) | 几乎无限(受物理内存限制) |
| 生命周期 | 函数作用域内 | 由可达性决定 |
| 并发访问 | 天然隔离 | 需要同步 |
| 碎片化 | 无 | 可能产生碎片 |
13.10.2 Go的分配策略
编译器优先在栈上分配。只有满足以下条件才分配到堆: 1. 变量在函数返回后仍被引用(逃逸) 2. 变量太大,栈放不下 3. 编译时无法确定大小13.10.3 减少堆分配的技巧
// ❌ 每次创建新切片,堆分配funcprocess(items[]int)[]int{result:=make([]int,0)// 逃逸for_,item:=rangeitems{result=append(result,item*2)}returnresult}// ✅ 预先分配,调用方控制内存funcprocessInPlace(items[]int){fori:=rangeitems{items[i]*=2}}// ✅ 传入输出切片funcprocessInto(dst,src[]int){fori,v:=rangesrc{dst[i]=v*2}}13.11 指针使用最佳实践
13.11.1 原则总结
// 1. 不要恐惧指针——Go的指针很安全(无指针运算)p:=&user p.Age=30// 语法糖,安全// 2. 优先用值传递小型结构体typePointstruct{X,Yfloat64}// 16字节,用值// 3. 大结构体和包含Mutex的用指针typeDatabasestruct{mu sync.Mutex conn*sql.DB// ... 大量字段}// 4. nil检查funcprocess(u*User){ifu==nil{return// 防御nil指针}fmt.Println(u.Name)}// 5. 不要对for-range循环变量长期持有指针fori:=rangeusers{gofunc(u*User){...}(&users[i])// ✅ 按索引取地址// go func(u *User) { ... }(&user) // ❌ user是副本}13.11.2 原则速查
| 原则 | 说明 |
|---|---|
| Go无指针运算 | 不能p++,安全! |
| nil检查 | 指针接收者方法需做nil防御 |
| 不长期持循环变量指针 | for-range变量会被复用 |
| 能用值就不用指针 | 简单类型、小结构体用值 |
| 不要返回局部变量的地址给C | Go的栈可能移动 |
13.12 综合案例:内存高效缓存
packagemainimport("fmt""sync""time")// Cache 内存缓存(必须用指针:包含Mutex)typeCachestruct{mu sync.RWMutex itemsmap[string]*CacheItem}typeCacheItemstruct{Value any Expiration time.Time}// NewCache 构造函数返回指针funcNewCache()*Cache{return&Cache{items:make(map[string]*CacheItem),}}// Set 存储(指针接收者:需要修改Cache)func(c*Cache)Set(keystring,value any,ttl time.Duration){c.mu.Lock()deferc.mu.Unlock()c.items[key]=&CacheItem{Value:value,Expiration:time.Now().Add(ttl),}}// Get 读取(指针接收者:Cache包含Mutex不能复制)func(c*Cache)Get(keystring)(any,bool){c.mu.RLock()deferc.mu.RUnlock()item,ok:=c.items[key]if!ok||time.Now().After(item.Expiration){returnnil,false}returnitem.Value,true}// 测试funcmain(){cache:=NewCache()// *Cachecache.Set("user:1","张三",5*time.Second)ifval,ok:=cache.Get("user:1");ok{fmt.Println("命中:",val)}}13.13 本章总结
| 知识点 | 要点回顾 |
|---|---|
| 指针基础 | &取地址,*解引用,*T是指针类型 |
| new vs make | new返回指针分配零值;make仅用于slice/map/chan |
| 值vs指针传递 | 所有参数都是值传递;大结构体/需修改时用指针 |
| 指针的指针 | **T类型,JSON反序列化等场景用到 |
| unsafe.Pointer | 通用指针,绕过类型安全,慎用! |
| 逃逸分析 | 编译器决定栈/堆分配;-gcflags="-m"查看 |
| 栈vs堆 | 栈快但小,堆慢但大;编译器优先栈分配 |
| 最佳实践 | nil检查、不长期持循环变量指针、含Mutex必须用指针 |
💬思考与互动:你在项目中是否关注过逃逸分析?有没有遇到过因为值传递导致的内存拷贝性能问题?欢迎分享你的性能优化经历!下一章我们将学习包与模块管理——Go工程化的核心。
本文是「Go入门到精通 2026」系列第13篇。上一篇:[12-Go 入门到精通-接口详解] 下一篇:[14-Go 入门到精通-包与模块管理]