Go 中的sync poll
· 阅读需 7 分钟
使用场景:频繁构造结构体,分配内存,可以考虑对象池.
sync.Pool 是 Go 语言 sync 包中用于缓存临时对象的同步原语,核心目标是减少内存分配和 GC 压力——通过复用已创建的对象,避免频繁创建/销毁相同类型对象带来的性能损耗。本文将从设计初衷、核心原理、源码实现和最佳实践四个维度,带你彻底掌握 sync.Pool。
一、为什么需要 sync.Pool?
在 Go 程序中,频繁创建短生命周期的对象(如临时的 []byte、struct 实例)会导致:
- 内存分配频繁:每次
new/make都会触发堆内存分配,增加运行时开销; - GC 压力大:大量临时对象会被 GC 标记、清理,拖慢程序执行效率。
sync.Pool 解决的核心问题:复用已创建的对象,让对象在 GC 前被重复利用,减少内存分配和 GC 次数。
典型使用场景:
- 高频创建的临时缓冲区(如 HTTP 服务中处理请求的
[]byte); - 序列化/反序列化时的临时结构体实例;
- 数据库连接池之外的轻量级对象复用(注意:
sync.Pool不适合做连接池,因为对象可能被 GC 回收)。
二、sync.Pool 基础使用
1. 核心 API
sync.Pool 的接口极其简洁,核心包含 3 个部分:
type Pool struct {
// New 函数:当 Pool 中无可用对象时,调用该函数创建新对象
// 注意:New 是可选的,若未设置且 Pool 为空,Get 会返回 nil
New func() any
}
// Get:从 Pool 中获取一个对象,若 Pool 为空则调用 New 创建(或返回 nil)
func (p *Pool) Get() any
// Put:将对象放回 Pool,供后续复用
func (p *Pool) Put(x any)
2. 基础示例
以复用 []byte 为例(高频场景):
package main
import (
"fmt"
"sync"
)
func main() {
// 初始化 Pool,指定 New 函数创建新的 []byte
var bufPool = sync.Pool{
New: func() any {
fmt.Println("创建新的缓冲区")
// 初始容量 1024,减少后续扩容
return make([]byte, 0, 1024)
},
}
// 第一次 Get:Pool 为空,调用 New 创建
buf1 := bufPool.Get().([]byte)
fmt.Printf("buf1 初始状态:len=%d, cap=%d\n", len(buf1), cap(buf1))
// 使用缓冲区
buf1 = append(buf1, "hello sync.Pool"...)
fmt.Printf("buf1 使用后:%s, len=%d\n", buf1, len(buf1))
// 重置缓冲区并放回 Pool(关键:放回前清理数据)
buf1 = buf1[:0]
bufPool.Put(buf1)
// 第二次 Get:复用已有的缓冲区
buf2 := bufPool.Get().([]byte)
fmt.Printf("buf2 复用状态:len=%d, cap=%d\n", len(buf2), cap(buf2))
// 验证对象复用(地址相同)
fmt.Printf("buf1 地址:%p, buf2 地址:%p\n", &buf1, &buf2)
}
输出结果:
创建新的缓冲区
buf1 初始状态:len=0, cap=1024
buf1 使用后:hello sync.Pool, len=14
buf2 复用状态:len=0, cap=1024
buf1 地址:0xc0000a6000, buf2 地址:0xc0000a6000
3. 关键特性
- 自动清理:
sync.Pool中的对象会在 GC 时被自动清理(每次 GC 都会清空 Pool),因此不能依赖 Pool 存储需要持久化的对象; - 无界性:Pool 没有容量限制,Put 的对象会一直存储直到 GC;
- 并发安全:Get/Put 方法都是并发安全的,可在多个 goroutine 中直接使用;
- 不保证获取:即使之前 Put 了对象,Get 也可能返回 nil(如对象已被 GC 清理),因此使用时需检查。
三、sync.Pool 源码实现剖析
sync.Pool 的底层实现(定义在 src/sync/pool.go)围绕“本地缓存+全局缓存”设计,核心目标是减少并发竞争。
1. 核心结构体
// Pool 核心结构
type Pool struct {
noCopy noCopy // 禁止拷贝(通过 vet 检查)
local unsafe.Pointer // 指向本地缓存数组(每个 P 一个)
localSize uintptr // 本地缓存数组的大小(等于 P 的数量)
victim unsafe.Pointer // 旧的本地缓存(GC 时的“受害者”缓存)
victimSize uintptr // victim 数组的大小
New func() any // 新建对象的函数
}
// 每个 P 对应的本地缓存
type poolLocal struct {
poolLocalInternal
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte // 缓存行对齐,避免伪共享
}
// 本地缓存的实际数据
type poolLocalInternal struct {
private any // 私有对象(当前 P 独占,无竞争)
shared poolChain // 共享链表(其他 P 可访问,有竞争)
}
2. Get 方法核心逻辑
Get 的优先级:私有对象 → 本地共享链表 → 其他 P 的共享链表 → victim 缓存 → New 函数
func (p *Pool) Get() any {
// 1. 获取当前 goroutine 绑定的 P
l, pid := p.pin()
// 2. 优先取私有对象(无竞争)
x := l.private
l.private = nil
if x == nil {
// 3. 从本地共享链表取第一个对象
x, _ = l.shared.popHead()
if x == nil {
// 4. 从其他 P 的共享链表偷取对象(减少空等)
x = p.getSlow(pid)
}
}
// 释放 P 的绑定
runtime_procUnpin()
// 5. 所有缓存都为空,调用 New 创建
if x == nil && p.New != nil {
x = p.New()
}
return x
}
3. Put 方法核心逻辑
Put 的优先级:私有对象(为空时) → 本地共享链表
func (p *Pool) Put(x any) {
if x == nil {
return // 不存储 nil 对象
}
// 1. 获取当前 P
l, _ := p.pin()
// 2. 私有对象为空则直接存入(无竞争)
if l.private == nil {
l.private = x
x = nil
}
// 3. 私有对象已存在,存入本地共享链表
if x != nil {
l.shared.pushHead(x)
}
// 释放 P
runtime_procUnpin()
}
