6. Go垃圾回收与性能优化

6.1 核心知识点

垃圾回收

  • 三色标记法
  • 写屏障
  • 并发标记和清除
  • STW优化

性能分析工具

  • pprof
  • trace
  • benchstat

性能优化技巧

  • 减少内存分配
  • 使用对象池
  • 避免不必要的拷贝
  • 优化算法和数据结构

6.2 常见面试题

Q1: 如何使用pprof进行性能分析?

pprof 是什么?

pprof 是 Go 内置的性能分析工具,通过采样的方式收集程序运行时的性能数据。

核心概念

分析类型作用使用场景
CPU Profile分析CPU耗时分布程序运行慢,找热点函数
Memory Profile分析内存分配内存占用高,找内存泄漏
Goroutine Profile分析goroutine状态goroutine泄漏,死锁
Block Profile分析阻塞操作并发性能问题

采样原理

CPU采样:每10ms记录一次当前正在执行的函数调用栈
         ↓
         函数A被采样到100次 = 该函数大约占用了1秒CPU时间

两种使用方式

  1. HTTP方式(线上服务推荐)
import _ "net/http/pprof"
 
func main() {
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()
    // 你的业务代码
}

访问方式:

# 浏览器查看
http://localhost:6060/debug/pprof/
 
# 命令行分析(推荐)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30  # CPU
go tool pprof http://localhost:6060/debug/pprof/heap                # 内存
go tool pprof http://localhost:6060/debug/pprof/goroutine           # goroutine
  1. 文件方式(本地测试)
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// 执行要分析的代码

pprof 常用命令

(pprof) top 10      # 显示耗时最多的10个函数
(pprof) list 函数名  # 查看函数逐行耗时
(pprof) web         # 生成调用图(需要graphviz)
(pprof) pdf         # 导出PDF报告

top 输出解读

      flat  flat%   sum%        cum   cum%
     1.20s 30.00% 30.00%      2.50s 62.50%  main.heavyWork
     ↑       ↑       ↑          ↑      ↑
   自身耗时  占比   累计占比   包含调用  包含调用占比

代码实现

package main
 
import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "os"
    "runtime"
    "runtime/pprof"
    "sync"
)
 
func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}
 
func heavyComputation() {
    for i := 0; i < 1000; i++ {
        fibonacci(30)
    }
}
 
func memoryIntensive() {
    var data [][]byte
    for i := 0; i < 1000; i++ {
        data = append(data, make([]byte, 1024*1024))
    }
    _ = data
}
 
func main() {
    go func() {
        fmt.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    
    fmt.Println("Profiling server started on :6060")
    
    var wg sync.WaitGroup
    
    wg.Add(1)
    go func() {
        defer wg.Done()
        heavyComputation()
    }()
    
    wg.Add(1)
    go func() {
        defer wg.Done()
        memoryIntensive()
    }()
    
    wg.Wait()
    
    fmt.Println("Goroutines:", runtime.NumGoroutine())
    
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Alloc = %v MiB\n", m.Alloc/1024/1024)
    fmt.Printf("TotalAlloc = %v MiB\n", m.TotalAlloc/1024/1024)
    fmt.Printf("Sys = %v MiB\n", m.Sys/1024/1024)
    
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    
    heavyComputation()
    
    f, _ = os.Create("mem.prof")
    runtime.GC()
    pprof.WriteHeapProfile(f)
    f.Close()
}

使用pprof分析:

go tool pprof cpu.prof
go tool pprof mem.prof

常见错误分析

  • 错误1:不理解pprof的采样机制
  • 错误2:过度关注pprof导致性能下降
  • 错误3:不会解读pprof报告

Q2: 如何优化Go程序的内存使用?

内存优化核心原则:减少分配次数 > 复用对象 > 减少单次分配大小

常见优化技巧

技巧做法效果
预分配切片make([]T, 0, 预估容量)避免扩容时重新分配
使用 sync.Pool复用临时对象减少 GC 压力
避免字符串拼接strings.Builder减少中间字符串分配
传指针而非值func(data *BigStruct)避免大对象拷贝
使用切片而非数组传递引用而非复制减少栈内存

sync.Pool 使用场景

适合:频繁创建销毁的临时对象(如 buffer、临时结构体)
不适合:长期持有的对象、有状态的对象

内存泄漏常见原因

  1. goroutine 泄漏(channel 阻塞导致 goroutine 无法退出)
  2. 全局 map 只增不删
  3. time.Ticker 忘记 Stop()
  4. 闭包引用导致对象无法回收

代码实现

package main
 
import (
    "fmt"
    "runtime"
    "sync"
)
 
// Data 结构体,包含一个较大的 []byte 字段
// 这种结构体如果频繁创建销毁,会给 GC 带来压力
type Data struct {
    ID    int
    Name  string
    Value []byte  // 1KB 的缓冲区
}
 
// sync.Pool 的使用方式
// New 函数:当 Pool 为空时,调用此函数创建新对象
var dataPool = sync.Pool{
    New: func() interface{} {
        return &Data{
            Value: make([]byte, 1024),  // 预分配 1KB
        }
    },
}
 
// ❌ 错误用法:返回对象后又放回 Pool,导致数据竞争
func processDataWithPoolWrong() *Data {
    data := dataPool.Get().(*Data)
    defer dataPool.Put(data)  // ❌ 错误!返回后对象可能被其他 goroutine 复用
    
    data.ID = 1
    data.Name = "Test"
    
    return data  // 返回的对象可能被其他人修改!
}
 
// ✅ 正确用法1:用完立即放回,不返回对象
func processDataWithPoolCorrect1() {
    data := dataPool.Get().(*Data)
    
    // 使用对象
    data.ID = 1
    data.Name = "Test"
    doSomething(data)
    
    // 重置对象状态!避免脏数据
    data.ID = 0
    data.Name = ""
    // 注意:Value 切片不需要重新分配,只需清零或保留
    
    dataPool.Put(data)  // 用完放回
}
 
// ✅ 正确用法2:深拷贝后放回(注意切片的处理!)
func processDataWithPoolCorrect2() Data {
    data := dataPool.Get().(*Data)
    
    data.ID = 1
    data.Name = "Test"
    
    // ⚠️ 注意:result := *data 只是浅拷贝!
    // ID、Name 会完整拷贝(值类型/不可变类型)
    // 但 Value []byte 切片只拷贝 (ptr, len, cap),底层数组仍然共享!
    
    // ✅ 正确做法:对切片进行深拷贝
    valueCopy := make([]byte, len(data.Value))
    copy(valueCopy, data.Value)
    
    result := Data{
        ID:    data.ID,
        Name:  data.Name,
        Value: valueCopy,  // 使用新分配的切片
    }
    
    // 重置并放回
    data.ID = 0
    data.Name = ""
    // data.Value 保留,下次复用(这就是 Pool 的意义)
    dataPool.Put(data)
    
    return result  // 返回完全独立的副本
}
 
// 不使用 Pool 的版本(对比用)
func processDataWithoutPool() *Data {
    return &Data{
        ID:    1,
        Name:  "Test",
        Value: make([]byte, 1024),  // 每次都分配新内存
    }
}
 
// 性能对比
func benchmark() {
    var m1, m2 runtime.MemStats
    
    // 测试不使用 Pool
    runtime.ReadMemStats(&m1)
    for i := 0; i < 10000; i++ {
        processDataWithoutPool()
    }
    runtime.ReadMemStats(&m2)
    fmt.Printf("Without pool - Alloc: %d MiB, TotalAlloc: %d MiB\n",
        (m2.Alloc-m1.Alloc)/1024/1024, 
        (m2.TotalAlloc-m1.TotalAlloc)/1024/1024)
    // 输出示例:Without pool - Alloc: 9 MiB, TotalAlloc: 10 MiB
    
    // 测试使用 Pool
    runtime.ReadMemStats(&m1)
    for i := 0; i < 10000; i++ {
        processDataWithPoolCorrect1()
    }
    runtime.ReadMemStats(&m2)
    fmt.Printf("With pool - Alloc: %d MiB, TotalAlloc: %d MiB\n",
        (m2.Alloc-m1.Alloc)/1024/1024, 
        (m2.TotalAlloc-m1.TotalAlloc)/1024/1024)
    // 输出示例:With pool - Alloc: 0 MiB, TotalAlloc: 0 MiB
}

常见错误与正确做法对比

错误问题描述正确做法
返回后放回defer Put(data) + return data 导致数据竞争要么不返回,要么复制后放回
忘记重置对象携带上次的脏数据Put 前清零关键字段
存大对象Pool 可能长期持有大对象只存适量大小的临时对象
跨请求复用不同请求共享同一对象每个请求独立 Get/Put

sync.Pool 清理机制

sync.Pool 不是永久缓存!
- 每次 GC 时,Pool 中的对象可能被清理
- Pool 只是"尽力而为"的复用,不保证对象一定存在
- 不要用 Pool 做永久缓存,用 map 或其他结构

什么时候用 sync.Pool

✅ 适合:
- HTTP 请求处理中的临时 buffer
- JSON 编解码的临时对象
- 日志格式化的 strings.Builder

❌ 不适合:
- 需要持久化的连接池(用 channel 实现)
- 有状态的对象
- 生命周期长的对象

Q3: 如何优化Go程序的CPU性能?

CPU优化核心原则:选对算法 > 减少计算量 > 利用并发

常见优化技巧

技巧做法效果
选择合适算法O(n²)→O(n log n)数量级提升
减少内存分配分配也消耗 CPU减少 GC 开销
避免反射用代码生成替代反射慢 10-100 倍
使用 sync.Map读多写少场景避免锁竞争
合理并发根据 CPU 核数设置 worker充分利用多核
内联优化小函数自动内联减少函数调用开销

并发优化要点

// 获取 CPU 核数
numCPU := runtime.NumCPU()
 
// CPU 密集型任务:worker 数 = CPU 核数
// IO 密集型任务:worker 数 = CPU 核数 × 2~10

性能分析定位热点

# 找到 CPU 瓶颈
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
 
# 进入后
(pprof) top 10    # 找到耗时最多的函数
(pprof) list 函数名  # 查看具体哪行慢

代码实现

package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
func sumSliceNaive(nums []int) int {
    sum := 0
    for i := 0; i < len(nums); i++ {
        sum += nums[i]
    }
    return sum
}
 
func sumSliceRange(nums []int) int {
    sum := 0
    for _, num := range nums {
        sum += num
    }
    return sum
}
 
func sumSliceParallel(nums []int, workers int) int {
    var wg sync.WaitGroup
    results := make(chan int, workers)
    
    chunkSize := len(nums) / workers
    
    for i := 0; i < workers; i++ {
        wg.Add(1)
        start := i * chunkSize
        end := start + chunkSize
        if i == workers-1 {
            end = len(nums)
        }
        
        go func(start, end int) {
            defer wg.Done()
            sum := 0
            for i := start; i < end; i++ {
                sum += nums[i]
            }
            results <- sum
        }(start, end)
    }
    
    go func() {
        wg.Wait()
        close(results)
    }()
    
    total := 0
    for result := range results {
        total += result
    }
    
    return total
}
 
func benchmark() {
    nums := make([]int, 10000000)
    for i := range nums {
        nums[i] = i
    }
    
    start := time.Now()
    sumSliceNaive(nums)
    fmt.Printf("Naive: %v\n", time.Since(start))
    
    start = time.Now()
    sumSliceRange(nums)
    fmt.Printf("Range: %v\n", time.Since(start))
    
    start = time.Now()
    sumSliceParallel(nums, 4)
    fmt.Printf("Parallel (4 workers): %v\n", time.Since(start))
}
 
func main() {
    benchmark()
}

常见错误分析

  • 错误1:过度并发导致性能下降
  • 错误2:不理解CPU缓存对性能的影响
  • 错误3:过早优化导致代码复杂