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

📅 2026/7/8 10:31:20 👁️ 阅读次数 📝 编程学习
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)// *int

13.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:=&v

13.3.1 new 的使用场景

// 创建结构体指针user:=new(User)// 等同 &User{}user.Name="张三"// 创建基本类型指针(较少用)flag:=new(bool)*flag=true

13.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, ...)
适用类型任意类型slicemapchan
返回值*T(指针)T(值本身,非指针)
初始化填充零值填充内部数据结构
能否直接用✅ 可以✅ 可以

13.4.2 为什么需要 make

slicemapchan这三种类型在底层是指向数据结构的指针,使用前必须初始化其内部结构:

// ❌ 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,容量10

13.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 = 42

13.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: x

13.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变量会被复用
能用值就不用指针简单类型、小结构体用值
不要返回局部变量的地址给CGo的栈可能移动

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 makenew返回指针分配零值;make仅用于slice/map/chan
值vs指针传递所有参数都是值传递;大结构体/需修改时用指针
指针的指针**T类型,JSON反序列化等场景用到
unsafe.Pointer通用指针,绕过类型安全,慎用!
逃逸分析编译器决定栈/堆分配;-gcflags="-m"查看
栈vs堆栈快但小,堆慢但大;编译器优先栈分配
最佳实践nil检查、不长期持循环变量指针、含Mutex必须用指针

💬思考与互动:你在项目中是否关注过逃逸分析?有没有遇到过因为值传递导致的内存拷贝性能问题?欢迎分享你的性能优化经历!下一章我们将学习包与模块管理——Go工程化的核心。


本文是「Go入门到精通 2026」系列第13篇。上一篇:[12-Go 入门到精通-接口详解] 下一篇:[14-Go 入门到精通-包与模块管理]