導入
Go言語の基礎学習を行う中で、公式ブログのスライス利用時の落とし穴として紹介されていた内容について、実際に検証。
動作環境
- Go 1.23.x windows/amd64
実行方法
git clone git@github.com:n1sp/go-slice-mem-lab.git
cd go-slice-mem-lab
go run .
実行結果
$ go run .
[start] Alloc = 0.19 MB # プログラム起動時
[after Bad] Alloc = 100.20 MB # Badケース実行後(メモリリークあり)
[after Good] Alloc = 0.20 MB # Goodケース実行後(最適化済み)
Alloc(runtime.MemStats.Alloc)について:
Goランタイムが管理するヒープ領域のうち、現在割り当て済みとして使用されているメモリ量を示す。
問題の本質
スライスは元の配列への参照を保持するため、Find関数で一部のデータを抽出した場合でも、元の配列全体がメモリ上に保持され続ける。これにより、GCが不要なメモリを解放できず、メモリリークが発生する。
解決方法
必要なデータのみを新しいスライスにコピーすることで、元の配列への参照を切断し、GCによる解放を可能にする。
実際の実装では、appendを使うことでより簡潔に記述できる
// 100MBの配列を作成
b := make([]byte, 100*1024*1024)
// 配列の中央付近にターゲット文字列を配置
copy(b[50_000_000:], []byte("12345"))
// Bad: 元の配列への参照が残る
// return digit.Find(b)
// Good (今回の検証):必要最小限の配列を用意してコピー
c := make([]byte, len(digit.Find(b)))
copy(c, digit.Find(b))
return c
// Good (実用時推奨): appendで新しいスライスにコピーして返す
return append([]byte{}, digit.Find(b)...)
この方法により、以下の処理を一行で実現できる:
- 必要最小限の容量を持つ新しいスライスの作成
-
Find結果のコピー - 元の配列への参照の切断
まとめ
スライスを部分的に利用する場合は、元の配列への参照が残らないよう注意が必要。
append([]byte{}, slice...)パターンを使うことで、簡潔かつ安全にメモリを管理できる。