惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
博客园 - 叶小钗
爱范儿
爱范儿
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
美团技术团队
T
Tailwind CSS Blog
博客园 - 司徒正美
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
博客园 - 【当耐特】
博客园 - 三生石上(FineUI控件)
V
Visual Studio Blog
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备
Go语言堆栈分配与逃逸分析深度解析
zhengwenfeng · 2025-06-15 · via 郑文峰的博客

# 1. 前言

# 1.1 为什么需要关注堆栈分配?

在Go语言中,内存分配主要有两种方式:

  • 栈分配:轻量快速,函数结束时自动释放,不产生垃圾
  • 堆分配:需要垃圾回收(GC)参与,开销较大

关键区别

  • 栈分配比堆分配快10-100倍
  • 栈分配不会增加GC压力
  • 堆分配的对象生命周期更长

# 1.2 什么是逃逸分析?

逃逸分析是Go编译器在编译时进行的一种优化技术,它会分析变量的生命周期和使用方式,自动决定将变量分配在栈上还是堆上。

# 2. 逃逸分析实战

# 2.1 如何查看逃逸分析结果

在下面代码中,将x的变量的地址返回出去了,这个时候会将x放到堆上。

func allocate() *int {
	x := 42
	return &x // x escapes to the heap
}

func main() {
	allocate()
}

1
2
3
4
5
6
7
8

通过添加参数-gcflags="-m"可以看到如下逃逸的信息

$ go build -gcflags="-m"  main.go
...
./main.go:4:2: moved to heap: x

1
2
3

# 2.2常见的逃逸场景

  1. 返回局部变量指针
func escape() *int {
    x := 10
    return &x // escapes
}

1
2
3
4

  1. 闭包使用局部变量
func closureEscape() func() int {
    x := 5
    return func() int { return x } // x escapes
}

1
2
3
4

  1. 接口类型转换
func toInterface(i int) interface{} {
    return i // escapes if type info needed at runtime
}

1
2
3

  1. 全局变量赋值
var global *int

func assignGlobal() {
    x := 7
    global = &x // escapes
}

1
2
3
4
5
6

  1. 大尺寸对象
func makeLargeSlice() []int {
    s := make([]int, 10000) // may escape due to size
    return s
}

1
2
3
4

# 3. 堆与栈的性能基准测试

type Data struct {
	A, B, C int
}

// 栈分配
func StackAlloc() Data {
    return Data{1, 2, 3} // stays on stack
}

// 堆分配
func HeapAlloc() *Data {
    return &Data{1, 2, 3} // escapes to heap
}

func BenchmarkStackAlloc(b *testing.B) {
    for b.Loop() {
        _ = StackAlloc()
    }
}

func BenchmarkHeapAlloc(b *testing.B) {
    for b.Loop() {
        _ = HeapAlloc()
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

通过运行会发现两者区别并不大,而且竟然没有堆的申请。这是因为编译器很聪明,它发现通过HeapAlloc返回的指针没有任何意义,所以也就把它放在了栈上。

$ go test -bench=. -benchmem .  
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkStackAlloc-12          1000000000               0.2373 ns/op          0 B/op          0 allocs/op
BenchmarkHeapAlloc-12           1000000000               0.2253 ns/op          0 B/op          0 allocs/op
PASS
ok      main/demo       1.176s

1
2
3
4
5
6
7
8
9

我需要强制让它分配到堆上,使用全局赋值,修改代码后如下

type Data struct {
	A, B, C int
}

var sink *Data

func HeapAllocEscape() {
	d := &Data{1, 2, 3}
	sink = d // d escapes to heap
}

func StackAlloc() Data {
	return Data{1, 2, 3} // stays on stack
}

func BenchmarkStackAlloc(b *testing.B) {
	for range b.N {
		_ = StackAlloc()
	}
}

func BenchmarkHeapAlloc(b *testing.B) {
	for range b.N {
		HeapAllocEscape()
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

运行结果如下,使用堆存储的开销:35倍的慢调用,24 字节的分配和 1 次垃圾回收的对象。

$ go test -bench=. -benchmem .  
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkStackAlloc-12          1000000000               0.2285 ns/op          0 B/op          0 allocs/op
BenchmarkHeapAlloc-12           147388731                8.117 ns/op          24 B/op          1 allocs/op
PASS
ok      main/demo       3.064s

1
2
3
4
5
6
7
8
9

# 4. 最佳实践

# 4.1 优化栈分配场景

  • GC 压力大的时候
  • 对于短期的小对象
  • 高频调用的函数内部

# 4.2 不必强求栈分配的场景

  • 工厂方法返回对象指针(Go惯用法)
  • 对象需要跨函数生命周期。
  • 不频繁创建的小对象
  • 优化会影响代码可读性时

# 5. 总结

  1. 逃逸分析是Go的重要优化手段,自动决定变量分配位置。
  2. 栈分配性能优势明显,适合短生命周期对象。
  3. 堆分配虽然较慢,但在某些场景下是必要且合理的。
  4. 优化时要平衡性能与代码质量,避免过度优化。