Go 是自动垃圾回收的语言,创建对象没有回收的心理负担。但如果你要开发高性能应用,就必须关注 GC 的影响——大量创建堆上的对象,会增加 GC 标记的时间和 STW(stop-the-world)的开销。对象池 是一种经典的优化手段:把不用的对象回收起来复用,减少堆分配和 GC 压力。Go 标准库提供了 sync.Pool 来实现这个目的。

这篇文章我们先讲 sync.Pool 的用法和原理,再扩展到连接池和 Worker Pool。

一、sync.Pool 基本用法

sync.Pool 的 API 非常简洁:

┌─────────────────────────────────────────────────────────────┐
│                       sync.Pool                              │
├─────────────────────────────────────────────────────────────┤
│  New    func() any     创建新对象的工厂函数(Pool 为空时调用)│
│  Get()  any            从池中取出一个对象                     │
│  Put(x any)            把对象放回池中                         │
└─────────────────────────────────────────────────────────────┘

一个典型的用法——复用 bytes.Buffer

var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)
    },
}

func process(data []byte) string {
    buf := bufPool.Get().(*bytes.Buffer) // 从池中取
    defer func() {
        buf.Reset()        // ✅ 必须重置状态
        bufPool.Put(buf)   // 放回池中
    }()

    buf.Write(data)
    buf.WriteString(" processed")
    return buf.String()
}
没有 Pool:                         有 Pool:

  每次调用                            第 1 次调用
  ┌───────────────┐                  ┌───────────────┐
  │ new(Buffer)   │ ← 堆分配        │ new(Buffer)   │ ← 堆分配(Pool 为空)
  │ 使用 Buffer   │                  │ 使用 Buffer   │
  │ Buffer 变垃圾 │ ← 等 GC 回收    │ Put 回 Pool   │ ← 回收复用
  └───────────────┘                  └───────────────┘

  第 N 次调用                         第 N 次调用
  ┌───────────────┐                  ┌───────────────┐
  │ new(Buffer)   │ ← 又一次堆分配  │ Get 从 Pool   │ ← 零分配!
  │ 使用 Buffer   │                  │ 使用 Buffer   │
  │ Buffer 变垃圾 │ ← 又等 GC      │ Put 回 Pool   │ ← 继续复用
  └───────────────┘                  └───────────────┘

二、标准库中的实际应用

sync.Pool 在 Go 标准库中被广泛使用。以 fmt 包为例:

// fmt 包内部使用 Pool 复用 pp 结构体(fmt 的打印状态)
var ppFree = sync.Pool{
    New: func() any { return new(pp) },
}

func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
    p := ppFree.Get().(*pp)
    p.doPrintf(format, a)
    n, err = w.Write(p.buf)
    p.free() // 内部调用 ppFree.Put(p)
    return
}

其它标准库中的使用:

┌──────────────┬──────────────────────────────────┐
│ 包            │ 池化的对象                        │
├──────────────┼──────────────────────────────────┤
│ fmt          │ 打印状态 pp                       │
│ encoding/json│ 编解码缓冲区                      │
│ net/http     │ 请求/响应的 bufio.Reader/Writer   │
│ regexp       │ 正则匹配器状态                    │
└──────────────┴──────────────────────────────────┘

三、实现原理

sync.Pool 的实现很精巧,核心设计目标是 高并发下的低锁争用

内部结构

┌──────────────────────────────────────────────────────────────────┐
│                     sync.Pool 内部结构                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  local  [P]poolLocal          ← 每个 P 一个本地池               │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  poolLocal[0]   poolLocal[1]   poolLocal[2]  ...     │       │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐        │       │
│  │  │ private   │  │ private   │  │ private   │        │       │
│  │  │ shared []  │  │ shared []  │  │ shared []  │        │       │
│  │  └───────────┘  └───────────┘  └───────────┘        │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
│  victim [P]poolLocal          ← 上一轮 GC 前的对象              │
│                                                                  │
│  New  func() any              ← 工厂函数                        │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

每个 P(Go 调度器的处理器)有自己的本地池:

  • private:当前 P 独占的一个对象,Get/Put 无需加锁
  • shared:一个双端队列,当前 P 从头部操作(无锁),其他 P 可以从尾部"偷"(需要 CAS)

Get 的查找路径

Get():
  ┌─ 1. 当前 P 的 private ──→ 有 ──→ 直接返回(无锁)
  ├─ 2. 当前 P 的 shared 头部 ──→ 有 ──→ 返回(无锁)
  ├─ 3. 其他 P 的 shared 尾部 ──→ 有 ──→ 返回(CAS)
  ├─ 4. victim 池(同上逻辑)──→ 有 ──→ 返回
  └─ 5. 都没有 ──→ 调用 New() 创建新对象

GC 与 victim 机制

关键点:sync.Pool 中的对象会被 GC 回收。

┌─ GC 第 1 轮 ──────────────────────────────────────────┐
│                                                        │
│  local 中的对象 ──→ 移到 victim                        │
│  victim 中的旧对象 ──→ 丢弃(被 GC 回收)              │
│                                                        │
├─ GC 第 2 轮 ──────────────────────────────────────────┤
│                                                        │
│  local 中的对象 ──→ 移到 victim                        │
│  victim 中的对象(上一轮的 local)──→ 丢弃              │
│                                                        │
└────────────────────────────────────────────────────────┘

这意味着:一个对象在 Pool 中最多存活两个 GC 周期。 victim 机制是一个缓冲——它让对象在被彻底回收前还有一次被 Get 到的机会,减少了 GC 后的冷启动开销。

四、使用 Pool 最常踩的 3 个坑

坑 1:Get 之后没有重置状态

Pool 中的对象是被复用的,上一次使用的状态可能还在:

// ❌ 没重置,buf 里可能有上次的数据
buf := bufPool.Get().(*bytes.Buffer)
buf.WriteString("hello")
bufPool.Put(buf) // buf 内容是 "hello"

buf2 := bufPool.Get().(*bytes.Buffer)
buf2.WriteString(" world")
fmt.Println(buf2.String()) // 可能输出 "hello world" 💀
// ✅ Get 之后立即重置
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() // 清除上次的数据
buf.WriteString("hello")

坑 2:把 Pool 当作对象存储

Pool 中的对象随时可能被 GC 回收,不能当作缓存或持久存储:

// ❌ 不要这样用
pool.Put(importantData) // 放入 Pool
// ... 时间过去,GC 发生 ...
data := pool.Get() // 可能是 nil(对象被 GC 回收了)

sync.Pool 是"临时对象池",不是"缓存"。 如果你需要持久存储,用 map + 锁,或者用专门的缓存库。

坑 3:Pool 中放入大对象不释放

如果偶尔有超大对象进入 Pool,之后每次 Get 到的都可能是这个大对象,内存居高不下:

// ❌ 大 slice 放回 Pool,导致内存浪费
buf := bufPool.Get().(*bytes.Buffer)
buf.Grow(10 * 1024 * 1024) // 扩容到 10MB
// ... 使用 ...
bufPool.Put(buf) // 10MB 的 Buffer 回到池中

// 之后 Get 到的都是 10MB 的 Buffer,即使只需要 1KB

解决方案:Put 之前检查大小,超过阈值就丢弃:

const maxBufSize = 64 * 1024 // 64KB

buf := bufPool.Get().(*bytes.Buffer)
defer func() {
    if buf.Cap() > maxBufSize {
        return // ✅ 太大了,不放回池中,让 GC 回收
    }
    buf.Reset()
    bufPool.Put(buf)
}()

五、连接池:Pool 做不到的事

sync.Pool 的对象会被 GC 回收,这对于 数据库连接、TCP 长连接 这类需要持久保持的资源来说是不可接受的。这些场景需要专门的连接池。

database/sql 连接池

Go 标准库的 database/sql 内置了连接池管理:

db, _ := sql.Open("mysql", "dsn...")

db.SetMaxOpenConns(25)  // 最大连接数
db.SetMaxIdleConns(10)  // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
db.SetConnMaxIdleTime(3 * time.Minute) // 空闲连接最大存活时间
┌──────────────────────────────────────────────────────────────┐
│                  database/sql 连接池                          │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  空闲连接队列 (idleConns)                                    │
│  ┌────┐ ┌────┐ ┌────┐                                      │
│  │conn│ │conn│ │conn│  ← 最多 MaxIdleConns 个               │
│  └────┘ └────┘ └────┘                                      │
│                                                              │
│  活跃连接计数: numOpen ≤ MaxOpenConns                        │
│                                                              │
│  等待队列 (connRequests)                                     │
│  ┌────────────┐                                              │
│  │ goroutine A │ ← 连接数达上限时排队等待                    │
│  │ goroutine B │                                             │
│  └────────────┘                                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

sync.Pool vs 连接池对比

特性sync.Pool连接池(database/sql 等)
对象生命周期随时可能被 GC持久保持
容量控制❌ 无上限✅ 可设最大连接数
健康检查❌ 无✅ 超时、心跳检测
适用对象临时缓冲区数据库连接、TCP 连接
等待机制❌ 直接 New✅ 排队等待可用连接

六、Worker Pool 模式

另一个常见的"池"是 Worker Pool(goroutine 池)——用固定数量的 goroutine 处理大量任务,避免无限制创建 goroutine 导致资源耗尽。

func workerPool(numWorkers int, tasks <-chan func()) {
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for task := range tasks {
                task()
            }
        }()
    }
    wg.Wait()
}

func main() {
    tasks := make(chan func(), 100)

    // 启动 Worker Pool
    go func() {
        workerPool(10, tasks) // 10 个 worker
    }()

    // 提交任务
    for i := 0; i < 1000; i++ {
        i := i
        tasks <- func() {
            fmt.Printf("处理任务 %d\n", i)
        }
    }
    close(tasks) // 所有任务提交完毕,关闭 channel
}
                    tasks channel
                    ┌─────────────┐
  提交任务 ────→    │ task1 task2  │ ...
                    └──────┬──────┘
              ┌────────────┼────────────┐
              ↓            ↓            ↓
         ┌────────┐   ┌────────┐   ┌────────┐
         │Worker 1│   │Worker 2│   │Worker 3│  ... (共 N 个)
         └────────┘   └────────┘   └────────┘

为什么需要 Worker Pool?

不用 Worker Pool:                   用 Worker Pool:
  1000 个任务 → 1000 个 goroutine     1000 个任务 → 10 个 goroutine
  • goroutine 创建/销毁开销大         • goroutine 数量可控
  • 内存占用高                         • 内存占用稳定
  • 可能触发调度器压力                 • 可以限制并发度

提示:开源库 gammazero/workerpoolpanjf2000/ants 提供了更完善的 Worker Pool 实现,支持动态调整大小、错误处理、优雅关闭等特性。

七、实战建议

  1. sync.Pool 只适合临时对象——不要存放需要持久保持的资源,GC 随时可能回收
  2. Get 之后必须重置状态——Pool 中的对象带着上次使用的数据
  3. Put 之前检查大小——防止超大对象占据池空间
  4. 连接池用专门的库——数据库用 database/sql,TCP/gRPC 用对应的连接池库
  5. 大量并发任务用 Worker Pool——控制 goroutine 数量,避免资源耗尽
# benchmark 验证 Pool 是否真的带来了性能提升
go test -bench=. -benchmem ./...

# 两个应该加入 CI 的命令
go vet ./...
go test -race ./...

sync.Pool 是一个精巧的优化工具——它通过 per-P 本地池和 victim 机制实现了高并发下的低锁争用。但它只适合"临时对象复用"这一个场景。连接池、任务池是不同的"池",需要用不同的方案。理解每种"池"的边界,才能用对地方。