内存性能优化
问题
Go 中如何减少内存分配,降低 GC 压力?
答案
核心原则
减少堆分配 = 减少 GC 压力 = 降低延迟
常见优化手段
| 手段 | 优化前 | 优化后 |
|---|---|---|
| 预分配 slice | var s []int | s := make([]int, 0, n) |
| strings.Builder | s += "a" | b.WriteString("a") |
| sync.Pool | 每次 new | 对象复用 |
避免 []byte ↔ string 转换 | 频繁转换 | 使用 unsafe 零拷贝 |
| 值类型嵌入 | 指针字段 | 值字段 |
| 数组代替 slice | []T | [N]T(编译时已知大小) |
实战
预分配 slice:
// ❌ 多次扩容,产生多次分配
func collect(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}
// ✅ 预分配容量
func collect(n int) []int {
result := make([]int, 0, n)
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}
sync.Pool 复用对象:
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process(data []byte) string {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
buf.Write(data)
return buf.String()
}
避免 string/[]byte 转换:
import "unsafe"
// 零拷贝 string → []byte(只读场景)
func stringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// 零拷贝 []byte → string
func bytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
检查工具
# 逃逸分析:查看哪些变量逃逸到堆上
go build -gcflags="-m" ./...
# Benchmark 内存分配
go test -bench=. -benchmem
# pprof heap profile
go tool pprof http://localhost:6060/debug/pprof/heap
常见面试问题
Q1: 什么情况会导致变量逃逸到堆?
答案:
- 返回指针:
return &obj - 发送到 channel:
ch <- &obj - 存入 interface:
var i interface{} = obj - 闭包引用:闭包中使用了外部变量
- 超大对象:栈放不下
详见 逃逸分析。
Q2: sync.Pool 的对象什么时候被回收?
答案:每次 GC 时。sync.Pool 有 victim cache(Go 1.13+),对象在两次 GC 后才被回收,但不能依赖对象一直存在。Pool 适合做临时缓存,不适合做持久存储。