跳到主要内容

减少内存分配

问题

如何减少 Go 程序的内存分配,降低 GC 压力?

答案

为什么要减少分配

每次堆分配都意味着:

  1. 分配器的开销(mcache → mcentral → mheap)
  2. GC 需要扫描和回收的对象增加
  3. CPU 缓存不友好

减少分配 = 更少 GC = 更低延迟 + 更高吞吐。

优化技巧

1. 预分配 Slice

// ❌ 频繁扩容
var result []int
for i := 0; i < n; i++ {
result = append(result, i) // 多次扩容,每次复制
}

// ✅ 预分配
result := make([]int, 0, n)
for i := 0; i < n; i++ {
result = append(result, i) // 不触发扩容
}

2. strings.Builder 拼接字符串

// ❌ 每次拼接创建新字符串
s := ""
for i := 0; i < 1000; i++ {
s += strconv.Itoa(i) // O(n²) 分配
}

// ✅ strings.Builder
var b strings.Builder
b.Grow(4000) // 预估容量
for i := 0; i < 1000; i++ {
b.WriteString(strconv.Itoa(i))
}
s := b.String()

3. 避免不必要的 []byte ↔ string 转换

// ❌ 每次转换都分配新内存
func contains(s string, sub string) bool {
return bytes.Contains([]byte(s), []byte(sub)) // 2 次分配
}

// ✅ 直接用 strings 包
func contains(s string, sub string) bool {
return strings.Contains(s, sub) // 0 分配
}

4. 用值类型代替指针

// ❌ 指针可能导致逃逸
type Point struct{ X, Y float64 }
func newPoint() *Point { return &Point{1, 2} } // 逃逸到堆

// ✅ 值类型在栈上
func newPoint() Point { return Point{1, 2} } // 留在栈上

5. 数组代替 Slice(已知小尺寸)

// ❌ slice 的底层数组可能分配在堆上
buf := make([]byte, 64)

// ✅ 数组在编译时确定大小,通常在栈上
var buf [64]byte

6. 复用对象(sync.Pool)

var bufPool = sync.Pool{
New: func() any { return make([]byte, 0, 4096) },
}

func process() {
buf := bufPool.Get().([]byte)[:0]
defer bufPool.Put(buf)
// 使用 buf...
}

7. 避免装箱(interface 参数)

// ❌ int 装箱为 interface{} 导致堆分配
func log(v any) { fmt.Println(v) }
log(42) // 42 被装箱

// ✅ 用泛型避免装箱
func log[T any](v T) { fmt.Println(v) }
log(42) // 不装箱

8. 减少 fmt.Sprintf

// ❌ fmt.Sprintf 内部大量分配
s := fmt.Sprintf("%d", n)

// ✅ strconv 直接转换
s := strconv.Itoa(n)
// strconv.FormatFloat, strconv.FormatBool 等

测量分配

func BenchmarkXxx(b *testing.B) {
b.ReportAllocs() // 报告分配统计
for i := 0; i < b.N; i++ {
// 被测代码
}
}
// 输出: BenchmarkXxx-8 5000000 256 ns/op 64 B/op 2 allocs/op
// ↑ ↑
// 每次字节数 每次分配次数
# 逃逸分析
go build -gcflags="-m" ./...

# pprof 查看分配热点
go tool pprof -alloc_objects http://localhost:6060/debug/pprof/heap

常见面试问题

Q1: Go 中哪些操作是"零分配"的?

答案

  • 栈上的局部变量(不逃逸)
  • 数组和固定大小的结构体(小于一定阈值)
  • strings.Builder 最终的 String()(Go 1.10+ unsafe 优化)
  • strconv.Itoastrconv.FormatInt 等小数字转换
  • 预分配的 slice 不触发 append 扩容

Q2: 如何写零分配的代码?

答案

核心原则:

  1. 避免逃逸——返回值不返回指针
  2. 预分配——slice 和 map 给够容量
  3. 复用对象——sync.Pool
  4. 避免 interface——用泛型或具体类型
  5. 避免 fmt——用 strconv

相关链接