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

目录

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

13.4.2 为什么需要 make

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

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 入门到精通-包与模块管理

相关推荐
码兄科技9 小时前
Java AI智能体开发实战:从零构建智能对话系统指南
java·开发语言·人工智能
问君能有几多愁~9 小时前
C++ 数据结构复习笔记
数据结构·c++·笔记
tntxia10 小时前
C++ 基础教程:从入门到精通
c++
Drone_xjw10 小时前
从 GDB 到 CDB:C/C++ 程序调试的两把“手术刀”
c语言·开发语言·c++
SL-staff10 小时前
智慧园区2000+设备统一管理:JVS-IoT如何降低运维成本40%
java·开发语言·物联网·智慧园区·设备管理·运维优化·jvs-iot
xurime11 小时前
Excelize 开源十周年,发布 2.11.0 版本
golang·开源·github·excel·导出·导入·excelize·基础库
2zcode12 小时前
基于MATLAB图像处理的饮料瓶灌装液位检测系统设计与实现
开发语言·图像处理·matlab
必须得开心呀13 小时前
QT解决中文乱码问题
开发语言·qt