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 を使えば、使い終わったオブジェクトを捨てずに保持しておき、次に必要になったときに再利用できます。
毎回 make でメモリ確保 → 使い終わったら GC が回収 → 高負荷時に GC が頻発
使い終わったら 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 を使った場合のほうがアロケーション回数が大幅に減っていることがわかります。高スループットな処理では、この差がパフォーマンスに大きく影響します。