コンテンツにスキップ

Golang sync.Pool はシルバーバレットではない

sync.Pool とは

sync.Pool はオブジェクトを使いまわすためのキャッシュ。 毎回 new()make() でアロケーションするのではなく、プールから取り出して使い終わったら返すことでGCの負荷を下げる。

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

// 取り出す
buf := pool.Get().(*bytes.Buffer)
buf.Reset() // 前の使用の残りをクリア

// 使う
buf.WriteString("hello")

// 返す
pool.Put(buf)

どんな場面で使うか

高スループットなサーバーで、短命な一時オブジェクトを大量に生成するケース。

典型例: - JSONエンコード/デコードのバッファ - ログのフォーマット用バッファ - 一時的なスライス

// HTTPハンドラで毎リクエストバッファをプールから取得
func handler(w http.ResponseWriter, r *http.Request) {
    buf := pool.Get().(*bytes.Buffer)
    defer pool.Put(buf)
    buf.Reset()

    json.NewEncoder(buf).Encode(response)
    w.Write(buf.Bytes())
}

なぜシルバーバレットでないのか

1. GCでプールが空になる

sync.Pool のオブジェクトはGCのたびにクリアされる可能性がある。 低負荷時はプールが空になりやすく、New でアロケーションが走ってしまう。

→ GCの頻度が高い環境では恩恵が減る

2. プール返却を忘れるとリーク

Put を呼び忘れるとオブジェクトがプールに戻らず、プールが機能しない。 defer pool.Put(obj) を徹底する必要がある。

3. Reset を忘れると前のデータが残る

バッファをプールから取り出したら必ず初期化する。 忘れると前のリクエストのデータが混入するバグが起きる。

buf := pool.Get().(*bytes.Buffer)
buf.Reset() // ← これを忘れると危険

4. 重いオブジェクトには向かない

DBコネクションのような重いリソースにはコネクションプール(database/sql の内部プール)を使うべき。 sync.Pool は軽量な一時オブジェクト専用。

5. 並行性の罠

sync.Pool はgoroutine-safeだが、取り出したオブジェクトを複数goroutineで共有してはいけない。 Get → 使用 → Put の間は1つのgoroutineが持つ。

使うべきかの判断基準

条件 sync.Pool使用
同じ型のオブジェクトを短命に大量生成する 向いている
GCプレッシャーが実際にボトルネック 向いている
オブジェクトの生存時間が長い 向かない
重いリソース(DB、ファイル) 向かない
まず計測していない まず計測する

最初からsync.Poolを入れるのではなく、プロファイリングでアロケーションがボトルネックと判明してから使う。