常见性能优化切入点有哪些(字符串拼接/内存分配/对象复用)?
一句话结论:性能问题要先量化再动手:用 pprof 定位热点,重点检查分配与拷贝、锁粒度、系统调用与 GC 压力;高频路径上最常见的三类改进是少分配、少拷贝、少锁,而字符串拼接、预分配容量与 sync.Pool 复用是三个最直接的落地手段。
核心切入点
- 字符串拼接:循环或高频拼接改用 strings.Builder(内部 []byte 追加),避免 + 反复生成新字符串;能预估长度时先 Grow 预分配。
- 避免无谓分配:能传切片/指针就不复制大结构体;用 go build -gcflags=-m 观察哪些变量逃逸到堆。
- 预分配容量:make([]T, 0, n)、make(map, n),append 前先估容量,减少扩容搬移。
- 对象复用:连接、缓冲等重对象用 sync.Pool;bytes.Buffer 用完 Reset 复用。
- 减少格式化开销:高频路径用 strconv 拼数字,别用 fmt.Sprintf(反射加分配)。
- 锁与并发:缩小临界区、读写锁、分片(shard)降低竞争;能用 atomic 的计数不用锁。
- GC 压力:调整 GOGC,或 Go 1.19+ 用 GOMEMLIMIT / debug.SetMemoryLimit 限制堆上限。
- 系统调用:批量读写、缓冲 IO、用连接池复用 TCP 连接。
三个高频手段示例
// 1) 预分配 + Builder
var sb strings.Builder
sb.Grow(1024)
for i := 0; i < 1000; i++ {
sb.WriteString("item")
}
// 2) sync.Pool 复用缓冲
var pool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
buf := pool.Get().(*bytes.Buffer)
buf.Reset()
defer pool.Put(buf)
// 3) strconv 代替 fmt
s := strconv.Itoa(id) // 别写 fmt.Sprintf("%d", id)
优化纪律
- 先 pprof、再 bench、最后才改代码;改完用 benchstat 对比验证。
- 不要为低频路径做复杂优化:复杂度上去后收益常常是负的。
- 池与缓存对象“出借”到多个 goroutine 会引入新竞争,注意归还纪律。
常见追问 / 记忆点
- 追问:sync.Pool 的对象会被 GC 清掉吗?——会,GC 时池可能被清空,适合“重建便宜、复用省事”的对象。
- 追问:所有逃逸都要消除吗?——不必,只有热点路径的高频分配才值得处理。
- 记忆点:先量后优;三大目标“少分配、少拷贝、少锁”;Builder、预分配、sync.Pool 三件套。