目录
- [🔵 Go 入门到精通:指针与内存](#🔵 Go 入门到精通:指针与内存)
-
- [13.1 指针是什么](#13.1 指针是什么)
- [13.2 取地址与解引用](#13.2 取地址与解引用)
-
- [13.2.1 `&` 取地址运算符](#13.2.1
&取地址运算符) - [13.2.2 `*` 解引用运算符](#13.2.2
*解引用运算符) - [13.2.3 完整示例](#13.2.3 完整示例)
- [13.2.1 `&` 取地址运算符](#13.2.1
- [13.3 new 函数:分配零值返回指针](#13.3 new 函数:分配零值返回指针)
-
- [13.3.1 new 的使用场景](#13.3.1 new 的使用场景)
- [13.3.2 new 很少单独使用](#13.3.2 new 很少单独使用)
- [13.4 make 与 new 的区别](#13.4 make 与 new 的区别)
-
- [13.4.1 核心区别](#13.4.1 核心区别)
- [13.4.2 为什么需要 make](#13.4.2 为什么需要 make)
- [13.4.3 速查表](#13.4.3 速查表)
- [13.5 值传递 vs 指针传递](#13.5 值传递 vs 指针传递)
-
- [13.5.1 值传递的代价](#13.5.1 值传递的代价)
- [13.5.2 选择决策表](#13.5.2 选择决策表)
- [13.5.3 基准测试参考](#13.5.3 基准测试参考)
- [13.6 指针的指针](#13.6 指针的指针)
-
- [13.6.1 实际应用:JSON反序列化](#13.6.1 实际应用:JSON反序列化)
- [13.7 指针与结构体方法](#13.7 指针与结构体方法)
-
- [13.7.1 语法回顾](#13.7.1 语法回顾)
- [13.7.2 编译器自动转换](#13.7.2 编译器自动转换)
- [13.7.3 接收者选择速查](#13.7.3 接收者选择速查)
- [13.8 unsafe.Pointer 简介](#13.8 unsafe.Pointer 简介)
-
- [13.8.1 指针类型层次](#13.8.1 指针类型层次)
- [13.8.2 基本用法示例](#13.8.2 基本用法示例)
- [13.8.3 unsafe 的合法使用场景](#13.8.3 unsafe 的合法使用场景)
- [13.9 逃逸分析](#13.9 逃逸分析)
-
- [13.9.1 什么是逃逸](#13.9.1 什么是逃逸)
- [13.9.2 查看逃逸分析](#13.9.2 查看逃逸分析)
- [13.9.3 常见逃逸场景](#13.9.3 常见逃逸场景)
- [13.9.4 逃逸分析的意义](#13.9.4 逃逸分析的意义)
- [13.10 栈与堆:分配决策](#13.10 栈与堆:分配决策)
-
- [13.10.1 栈 vs 堆对比](#13.10.1 栈 vs 堆对比)
- [13.10.2 Go的分配策略](#13.10.2 Go的分配策略)
- [13.10.3 减少堆分配的技巧](#13.10.3 减少堆分配的技巧)
- [13.11 指针使用最佳实践](#13.11 指针使用最佳实践)
-
- [13.11.1 原则总结](#13.11.1 原则总结)
- [13.11.2 原则速查](#13.11.2 原则速查)
- [13.12 综合案例:内存高效缓存](#13.12 综合案例:内存高效缓存)
- [13.13 本章总结](#13.13 本章总结)
🔵 Go 入门到精通:指针与内存
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处
🧑💻 作者:布朗克168 | 系列:Go入门到精通 2026 · 第三阶段 · 核心进阶
13.1 指针是什么
指针是存储另一个变量内存地址的变量。通俗地说,指针告诉你"某个值在哪里"而非"那个值是什么"。
go
x := 42
p := &x // p 是指向 x 的指针,存储了 x 的地址
fmt.Println(x) // 42
fmt.Println(p) // 0xc0000140a0(某个内存地址)
fmt.Println(*p) // 42(通过指针读取 x 的值)
内存模型图:
变量 x (地址: 0xc0000140a0)
┌─────────────────────┐
│ 42 │ ← x 的值
└─────────────────────┘
↑
│ 指针 p 存储了这个地址
│
变量 p
┌─────────────────────┐
│ 0xc0000140a0 │ ← p 的值 = x 的地址
└─────────────────────┘
| 概念 | 说明 | 类比 |
|---|---|---|
| 值 | 直接存储的数据 | 你的家 |
| 地址 | 数据存储的位置 | 你家的门牌号 |
| 指针 | 存储地址的变量 | 记着门牌号的纸条 |
| 解引用 | 通过地址访问数据 | 按门牌号找到你的家 |
13.2 取地址与解引用
13.2.1 & 取地址运算符
go
var x int = 100
p := &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 * 解引用运算符
go
*p = 200 // 通过指针修改 x 的值
fmt.Println(x) // 200(x 被修改了!)
fmt.Println(*p) // 200
// * 的双重身份:
// 1. 在类型声明中:*int 表示"指向int的指针"类型
// 2. 在表达式中:*p 表示"p所指向的值"(解引用)
13.2.3 完整示例
go
func swap(a, b *int) {
*a, *b = *b, *a // 通过指针交换值
}
func main() {
x, y := 10, 20
swap(&x, &y)
fmt.Println(x, y) // 20, 10 ✅
}
13.3 new 函数:分配零值返回指针
new(T) 是Go内置函数,用于分配内存:
go
p := new(int) // p 是 *int 类型
fmt.Println(*p) // 0(零值)
*p = 42
fmt.Println(*p) // 42
// new(T) 等价于以下两步:
var v T
p := &v
13.3.1 new 的使用场景
go
// 创建结构体指针
user := new(User) // 等同 &User{}
user.Name = "张三"
// 创建基本类型指针(较少用)
flag := new(bool)
*flag = true
13.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 这三种类型在底层是指向数据结构的指针,使用前必须初始化其内部结构:
go
// ❌ nil map 不能写入
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
// ✅ 用 make 初始化
m = make(map[string]int)
m["key"] = 1 // OK
// ❌ nil slice 不能追加(其实可以,但不推荐)
var s []int
s = append(s, 1) // 实际上可以,但容量为0
// ✅ 用 make 预分配
s = make([]int, 0, 10) // 长度0,容量10
13.4.3 速查表
go
// new:返回指针
p := new(int) // *int, *p = 0
u := new(User) // *User, u.Name = ""
// make:返回值
s := make([]int, 5, 10) // []int, len=5, cap=10
m := make(map[string]int) // map[string]int
ch := make(chan int, 10) // chan int, 缓冲区大小10
🔑 记忆口诀:"new给所有类型分配零值指针,make只为三兄弟(slice/map/chan)初始化。"
13.5 值传递 vs 指针传递
Go中所有函数参数都是值传递------即使传指针,也是传递了指针值的副本。
13.5.1 值传递的代价
go
type BigStruct struct {
Data [1024 * 1024]byte // 1MB
}
func processValue(bs BigStruct) { // ❌ 传递时拷贝 1MB
// ...
}
func processPointer(bs *BigStruct) { // ✅ 只传递 8 字节指针
// ...
}
13.5.2 选择决策表
| 场景 | 推荐 | 原因 |
|---|---|---|
| 需要修改原值 | 指针 | 值传递无法修改 |
| 结构体 > 64字节 | 指针 | 避免大内存拷贝 |
| 结构体含Mutex等不可复制字段 | 指针 | 防止意外复制锁 |
| 小型不可变结构体 | 值 | 更安全、无nil风险 |
| 基本类型(int, float64等) | 值 | 拷贝成本极低(8字节) |
| 需要并发安全隔离 | 值 | 每个goroutine独立副本 |
13.5.3 基准测试参考
go
// 小结构体(24字节)
type Small struct{ a, b, c int64 }
// 大结构体(~1KB)
type Large struct{ data [128]int64 }
| 结构体大小 | 值传递 | 指针传递 | 建议 |
|---|---|---|---|
| ≤64字节 | 更快 | 略慢(需解引用) | 用值 |
| >64字节 | 慢(大量拷贝) | 快 | 用指针 |
| 含Mutex | 编译报错/运行时竞态 | 正确 | 必须用指针 |
⚠️ 注意 :
sync.Mutex等同步原语不能复制 (go vet会警告),包含它们的结构体必须用指针传递。
13.6 指针的指针
指针本身也是值,因此可以有指向指针的指针:
go
x := 42
p := &x // *int, 指向 x
pp := &p // **int, 指向 p(指针的指针)
fmt.Println(x) // 42
fmt.Println(*p) // 42
fmt.Println(**pp) // 42(两次解引用)
**pp = 100
fmt.Println(x) // 100 ✅
内存布局:
x (42) ← p (存储&x) ← pp (存储&p)
[ 42 ] [ &x ] [ &p ]
*p = 42 *pp = &x
**pp = 42
13.6.1 实际应用:JSON反序列化
go
var config *ServerConfig
json.Unmarshal(data, &config) // 需要传 **ServerConfig
// ↑ 因为 config 本身就是指针
// &config 是指针的指针
13.7 指针与结构体方法
这在第11章已经详细讨论过,此处做系统总结:
13.7.1 语法回顾
go
type Counter struct{ count int }
// 指针接收者:能修改原值
func (c *Counter) Increment() { c.count++ }
// 值接收者:不能修改原值
func (c Counter) Count() int { return c.count }
13.7.2 编译器自动转换
go
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 基本用法示例
go
import "unsafe"
// 在不同类型的指针间转换(极其危险!)
var f float64 = 3.14
p := unsafe.Pointer(&f) // *float64 → unsafe.Pointer
ip := (*int64)(p) // unsafe.Pointer → *int64
fmt.Println(*ip) // 浮点数的底层bit表示
// 访问结构体未导出字段(绕过封装)
type User struct {
name string // 小写,外部不可见
age int
}
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 什么是逃逸
go
// 不逃逸:分配在栈上
func stayOnStack() int {
x := 42
return x // x的生命周期在函数内,分配在栈上
}
// 逃逸:分配在堆上
func escapeToHeap() *int {
x := 42
return &x // x 的地址被返回,x "逃逸"到堆上
}
13.9.2 查看逃逸分析
bash
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 减少堆分配的技巧
go
// ❌ 每次创建新切片,堆分配
func process(items []int) []int {
result := make([]int, 0) // 逃逸
for _, item := range items {
result = append(result, item*2)
}
return result
}
// ✅ 预先分配,调用方控制内存
func processInPlace(items []int) {
for i := range items {
items[i] *= 2
}
}
// ✅ 传入输出切片
func processInto(dst, src []int) {
for i, v := range src {
dst[i] = v * 2
}
}
13.11 指针使用最佳实践
13.11.1 原则总结
go
// 1. 不要恐惧指针------Go的指针很安全(无指针运算)
p := &user
p.Age = 30 // 语法糖,安全
// 2. 优先用值传递小型结构体
type Point struct{ X, Y float64 } // 16字节,用值
// 3. 大结构体和包含Mutex的用指针
type Database struct {
mu sync.Mutex
conn *sql.DB
// ... 大量字段
}
// 4. nil检查
func process(u *User) {
if u == nil {
return // 防御nil指针
}
fmt.Println(u.Name)
}
// 5. 不要对for-range循环变量长期持有指针
for i := range users {
go func(u *User) { ... }(&users[i]) // ✅ 按索引取地址
// go func(u *User) { ... }(&user) // ❌ user是副本
}
13.11.2 原则速查
| 原则 | 说明 |
|---|---|
| Go无指针运算 | 不能 p++,安全! |
| nil检查 | 指针接收者方法需做nil防御 |
| 不长期持循环变量指针 | for-range变量会被复用 |
| 能用值就不用指针 | 简单类型、小结构体用值 |
| 不要返回局部变量的地址给C | Go的栈可能移动 |
13.12 综合案例:内存高效缓存
go
package main
import (
"fmt"
"sync"
"time"
)
// Cache 内存缓存(必须用指针:包含Mutex)
type Cache struct {
mu sync.RWMutex
items map[string]*CacheItem
}
type CacheItem struct {
Value any
Expiration time.Time
}
// NewCache 构造函数返回指针
func NewCache() *Cache {
return &Cache{
items: make(map[string]*CacheItem),
}
}
// Set 存储(指针接收者:需要修改Cache)
func (c *Cache) Set(key string, value any, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = &CacheItem{
Value: value,
Expiration: time.Now().Add(ttl),
}
}
// Get 读取(指针接收者:Cache包含Mutex不能复制)
func (c *Cache) Get(key string) (any, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok || time.Now().After(item.Expiration) {
return nil, false
}
return item.Value, true
}
// 测试
func main() {
cache := NewCache() // *Cache
cache.Set("user:1", "张三", 5*time.Second)
if val, 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 入门到精通-包与模块管理