Go の sync.Pool でオブジェクトを再利用する

sync.Pool は、一時的なオブジェクトを再利用するための仕組みです。頻繁にオブジェクトを生成・破棄する処理では、Pool を使うことでメモリ割り当ての回数を減らし、GC の負荷を軽減できます。

sync.Pool の基本

Pool には Put でオブジェクトを返却し、Get で取得します。Pool が空の場合は New 関数が呼ばれて新しいオブジェクトが生成されます。

package main

import (
    "fmt"
    "sync"
)

func main() {
    pool := &sync.Pool{
        New: func() interface{} {
            fmt.Println("新規作成")
            return make([]byte, 1024)
        },
    }

    // 最初の Get は Pool が空なので New が呼ばれる
    buf1 := pool.Get().([]byte)
    fmt.Printf("buf1 取得: len=%d\n", len(buf1))

    // 返却
    pool.Put(buf1)

    // 2回目の Get は返却されたものを再利用
    buf2 := pool.Get().([]byte)
    fmt.Printf("buf2 取得: len=%d\n", len(buf2))
}

実行すると「新規作成」は1回しか表示されません。2回目の Get では返却済みのバッファが再利用されているためです。

なぜ Pool を使うのか

高頻度で短命なオブジェクトを扱う処理では、毎回新規にメモリを割り当てると GC の負担が増えます。Pool を使えば、使い終わったオブジェクトを捨てずに保持しておき、次に必要になったときに再利用できます。

Pool なし

毎回 make でメモリ確保 → 使い終わったら GC が回収 → 高負荷時に GC が頻発

Pool あり

使い終わったら Put で返却 → 次回 Get で再利用 → メモリ割り当て回数が減り GC 負荷も軽減

実践例:バッファの再利用

HTTP サーバーでリクエストごとにバッファを使う場合、Pool が効果的です。

package main

import (
    "bytes"
    "fmt"
    "sync"
)

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

func processRequest(data string) string {
    // Pool からバッファを取得
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset() // 中身をクリア
        bufferPool.Put(buf) // 返却
    }()

    buf.WriteString("処理結果: ")
    buf.WriteString(data)
    return buf.String()
}

func main() {
    results := make([]string, 3)
    var wg sync.WaitGroup

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            results[i] = processRequest(fmt.Sprintf("データ%d", i))
        }(i)
    }

    wg.Wait()
    for _, r := range results {
        fmt.Println(r)
    }
}

defer で確実に返却処理を行うのがポイントです。返却を忘れると Pool の意味がなくなってしまいます。

Pool の注意点

Pool に入れたオブジェクトは、いつ GC に回収されるかわかりません。Go のランタイムは GC のタイミングで Pool の中身を自動的にクリアすることがあります。そのため、Pool は「あれば再利用、なければ新規作成」というキャッシュ的な用途に適しています。

永続的な保存には使わない

Pool はキャッシュであり、確実に保持されるわけではない。重要なデータの保存場所としては不適切。

返却前にリセットする

前回の使用で残ったデータが混入しないよう、Put する前に中身をクリアするのが安全。

ベンチマークで効果を確認

Pool の効果はベンチマークで確認できます。

package main

import (
    "sync"
    "testing"
)

var pool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func BenchmarkWithoutPool(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buf := make([]byte, 1024)
        _ = buf
    }
}

func BenchmarkWithPool(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buf := pool.Get().([]byte)
        pool.Put(buf)
    }
}

go test -bench=. を実行すると、Pool を使った場合のほうがアロケーション回数が大幅に減っていることがわかります。高スループットな処理では、この差がパフォーマンスに大きく影響します。