Go 入门到精通-13-指针与内存

📅 2026/7/8 10:16:35
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 为什么需要 make13.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取地址运算符varxint100p: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*解引用运算符*p200// 通过指针修改 x 的值fmt.Println(x)// 200x 被修改了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零值*p42fmt.Println(*p)// 42// new(T) 等价于以下两步varv T p:v13.3.1 new 的使用场景// 创建结构体指针user:new(User)// 等同 User{}user.Name张三// 创建基本类型指针较少用flag:new(bool)*flagtrue13.3.2 new 很少单独使用方式代码使用频率newu : 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 为什么需要 makeslice、map、chan这三种类型在底层是指向数据结构的指针使用前必须初始化其内部结构// ❌ nil map 不能写入varmmap[string]intm[key]1// panic: assignment to entry in nil map// ✅ 用 make 初始化mmake(map[string]int)m[key]1// OK// ❌ nil slice 不能追加其实可以但不推荐vars[]intsappend(s,1)// 实际上可以但容量为0// ✅ 用 make 预分配smake([]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, len5, cap10m: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}// 大结构体~1KBtypeLargestruct{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两次解引用**pp100fmt.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 基本用法示例importunsafe// 在不同类型的指针间转换极其危险varffloat643.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:42returnx// x 的地址被返回x 逃逸到堆上}13.9.2 查看逃逸分析go build-gcflags-mmain.go# 或更详细go build-gcflags-m -mmain.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{resultappend(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.Age30// 语法糖安全// 2. 优先用值传递小型结构体typePointstruct{X,Yfloat64}// 16字节用值// 3. 大结构体和包含Mutex的用指针typeDatabasestruct{mu sync.Mutex conn*sql.DB// ... 大量字段}// 4. nil检查funcprocess(u*User){ifunil{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变量会被复用能用值就不用指针简单类型、小结构体用值不要返回局部变量的地址给CGo的栈可能移动13.12 综合案例内存高效缓存packagemainimport(fmtsynctime)// Cache 内存缓存必须用指针包含MutextypeCachestruct{mu sync.RWMutex itemsmap[string]*CacheItem}typeCacheItemstruct{Value any Expiration time.Time}// NewCache 构造函数返回指针funcNewCache()*Cache{returnCache{items:make(map[string]*CacheItem),}}// Set 存储指针接收者需要修改Cachefunc(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 makenew返回指针分配零值make仅用于slice/map/chan值vs指针传递所有参数都是值传递大结构体/需修改时用指针指针的指针**T类型JSON反序列化等场景用到unsafe.Pointer通用指针绕过类型安全慎用逃逸分析编译器决定栈/堆分配-gcflags-m查看栈vs堆栈快但小堆慢但大编译器优先栈分配最佳实践nil检查、不长期持循环变量指针、含Mutex必须用指针思考与互动你在项目中是否关注过逃逸分析有没有遇到过因为值传递导致的内存拷贝性能问题欢迎分享你的性能优化经历下一章我们将学习包与模块管理——Go工程化的核心。本文是「Go入门到精通 2026」系列第13篇。上一篇[12-Go 入门到精通-接口详解] 下一篇[14-Go 入门到精通-包与模块管理]