go-data-structures について
Go 開発者向けに、スライス、マップ、配列などの選択と使い方をまとめたスキルです。エイリアス、nil 値、反復処理、メモリ保持に関する不具合を避けるための指針を示し、詳細なコンテナ資料は必要に応じて参照できます。
主な機能
- 01nil と空スライスの指針
- 02append のエイリアスとコピー規則
- 03スライスとマップの事前割り当て
- 04マップ、セット、配列の選択
- 05コンテナとメモリの挙動
- 06確認チェックリスト
活用シーン
- 01Go コードのスライスエイリアス確認
- 02キューやキャッシュのデータ構造選択
- 03サイズが既知の場合の割り当て削減
- 04マップとスライスのメモリ挙動の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-data-structuresインストール後、Claude Code で/go-data-structuresを入力して読み込まれたか確認します。
同梱ファイルreferences/containers.md
詳細
Go のデータ構造
スライスとマップは単純に見えますが、言語の中でも特に厄介な落とし穴が潜んでいます。 次のルールに従えば、コードレビューをすり抜けるエイリアシング、nil、反復処理のバグを防げます。
詳細なリファレンス資料は、必要に応じて読み込んでください。
references/containers.md—container/heapの優先度キュー、container/listを使った LRU、マップのメモリ再利用、weak.Pointerキャッシュ。
以下の概要だけでは不十分な場合にのみ、リファレンスファイルを読んでください。
1. nil スライスと空のスライス
var a []int // nil slice — len 0, cap 0, no allocation
b := []int{} // empty slice — len 0, allocated header
c := make([]int, 0) // empty slice — same as b
len、cap、range、appendは、3 つすべてを同じように扱います。- 「要素なし」を表す値には、nil スライスを優先してください。「空」を返すためだけに割り当てを行わないでください。
- 例外:JSON の場合です。
encoding/jsonでは、nil はnullに、空のスライスは[]にマーシャリングされます。契約上[]が必要な場合は、空のスライスを返してください。Go 1.27 のencoding/json/v2API では、デフォルトで nil は[]にマーシャリングされます。 - ロジック内で nil と空を区別しないでください。
len(s) == 0を確認してください。
2. append の動作とエイリアシング
append は、同じバッキング配列を返す場合も、新しい配列を返す場合もあります。どちらの場合も問題が起こりえます。
// ❌ Bad — result may alias the input
func addSuffix(base []string) []string {
return append(base, "suffix") // if cap(base) > len(base),
} // this WRITES INTO base's array
// ✅ Good — force a copy when the input must not be touched
func addSuffix(base []string) []string {
out := make([]string, len(base), len(base)+1)
copy(out, base)
return append(out, "suffix")
}
// ❌ Bad — subslice keeps the whole 64 MB alive
func header(big []byte) []byte {
return big[:512] // backing array is still the full big
}
// ✅ Good — copy the window you keep
func header(big []byte) []byte {
return slices.Clone(big[:512]) // Go 1.21+; or copy() manually
}
ルール:関数はスライスを所有するか、コピーするかのどちらかにしてください。呼び出し元のスライスのサブスライスを返したり、そのスライスに追加したりすると、メモリが暗黙的に共有されます。
3. 事前割り当て
最終サイズが分かっている場合、または上限が分かっている場合は、一度だけ割り当てます。
// ✅ Good — one allocation
names := make([]string, 0, len(users))
for _, u := range users {
names = append(names, u.Name)
}
// ❌ Bad — repeated growth and copying
var names []string
for _, u := range users {
names = append(names, u.Name)
}
マップも同様です:make(map[string]int, len(items)。
サイズが分からない場合は事前割り当てをしないでください。大きすぎる容量を指定するとメモリを無駄にします。実行頻度の低いパスでは、append による拡張で問題ありません。
4. マップの基本
// Comma-ok distinguishes "missing" from "zero value"
count, ok := hits[key]
if !ok { /* key absent */ }
// Zero value reads are safe; writes to a nil map PANIC
var m map[string]int
_ = m["x"] // 0, fine
m["x"] = 1 // panic: assignment to entry in nil map — make() first
// Iteration order is RANDOM and differs between runs.
// Sort keys when output must be deterministic:
keys := slices.Sorted(maps.Keys(m)) // Go 1.23+
for _, k := range keys {
fmt.Println(k, m[k])
}
- マップの値はアドレス指定できません。構造体の値に対する
m[k].Field = vはコンパイルできません。ポインターのマップを使うか、読み取り・変更・書き戻しを行ってください。 range中の削除は安全です。range中の挿入は未定義です(新しいキーが反復対象になる場合とならない場合があります)。
5. セット
慣用的なセットは、値に空の構造体を使うマップです。
seen := make(map[string]struct{}, len(items))
for _, it := range items {
if _, dup := seen[it.ID]; dup {
continue
}
seen[it.ID] = struct{}{}
process(it)
}
struct{} は 0 バイトを占めます。if seen[id] のようにメンバーシップを確認する場合は、map[string]bool も使え、こちらのほうが読みやすくなります。
6. 配列とスライス
- 配列(
[4]byte)は値です。代入や引数渡しでは配列全体がコピーされます。要素が比較可能であれば、==で比較できます。 - 値セマンティクスを持つ固定サイズのデータには配列を使ってください。例:ハッシュ(
[32]byte)、IPv4 アドレス、固定サイズの行列、マップのキー。 - それ以外はすべてスライスです。
[100]intを受け取る関数は、呼び出しごとに 800 バイトをコピーします。ほとんどの場合、これは誤りです。
7. 構造の選び方
| 用途 | 使用するもの |
|---|---|
| 順序付きの拡張可能なコレクション | []T |
| メンバーシップ確認/重複排除 | map[K]struct{} |
| キー→値の検索 | map[K]V |
| 固定サイズ、値セマンティクス、比較可能 | [N]T 配列 |
| FIFO キュー(単一のゴルーチン) | head インデックス付きスライス、または頻繁な追加・削除には container/list |
| スタック | スライス + append / s[:len(s)-1] |
| 並行マップ、書き込み後は読み取りのみのキー | sync.Map — それ以外はミューテックス + マップ |
| 優先度キュー、トップ K、スケジューラー | container/heap |
| 固定サイズのローリングウィンドウ | リングとして使うスライス(インデックスを len で剰余演算)、または container/ring |
| LRU キャッシュ | map[K]*list.Element + container/list |
| 値を生存させ続けてはならないキャッシュ | map[K]weak.Pointer[V](Go 1.24 以降) |
| 同一文字列の大量の重複排除 | unique.Make(Go 1.23 以降) |
| カスタムハッシュテーブルのキー型 | maphash.Comparable / maphash.Hasher(1.24 / 1.27) |
sync.Map は特殊な用途向けのツールです(追記専用キャッシュ、互いに重ならないキーセットなど)。基本的には map + sync.RWMutex を使ってください。ロックのパターンについては、並行処理のスキルを参照してください。
8. メモリの動作
- マップは縮小しません。
deleteとclearはバケットを保持します。かつて数百万のエントリーを保持していたマップがほぼ空になった場合は、メモリを解放するために再構築してください(m = maps.Clone(m))。 clear(s)はスライスの要素をゼロ値にしますが、長さは維持します。s = s[:0]は再利用のためにバッキング配列を保持します。どちらもメモリを解放しません。- ポインターのスライスから要素を取り出す場合 — 空いたスロットを nil にしてください(
s[n-1] = nil)。そうしないと、GC はオブジェクトが到達可能だと認識し続けます。 strings.Builderは最後にコピーせずに文字列を構築します。一度書き込みを行った後はコピーしてはいけません。bytes.Bufferはバイト指向の I/O に使う型です(io.Readerとio.Writerの両方です)。そのString()はコピーを行います。
9. コピーのセマンティクス
| 型 | 代入でコピーされるもの | 独立したコピー |
|---|---|---|
| 基本型、配列、値を持つ構造体 | すべて | すでに独立しています |
| スライス | ヘッダー(ptr、len、cap) | slices.Clone |
| マップ | 参照 | maps.Clone(浅いコピー) |
| ポインター、チャネル、関数 | 参照 | — |
| インターフェース | 型 + 値のワード | 動的型によります |
クローンは浅いコピーです。クローンした []*User も、すべての User を共有します。
セクション 7~9 の例:references/containers.md。
検証チェックリスト
- nil スライスと空スライスを区別するロジックがなく、空かどうかの判定には
len()を使用している - 契約で
[]が必要な場合、JSON に渡すスライスを明示的に空(nil ではない)にしている - 関数が所有していないスライスに
appendしていない。コピーを明示的に行っている slices.Clone/copyを使わず、大きな配列のサブスライスを長期間保持していない- サイズが分かっている場合、スライスとマップを容量を指定して事前確保している
- 「存在しない」こととゼロ値が異なる場合は、必ず comma-ok を使用している
- nil の可能性があるマップに書き込んでいない
- 出力パスを決定的にする場合、反復の前にマップのキーをソートしている
- セットは
map[K]struct{}として構築している(読みやすさを優先する場合はmap[K]bool) - 値セマンティクスまたは比較可能性が目的の場合に限り、配列を使用している
- 大幅に縮小した長期間保持するマップは、要素を削除するだけでなく再構築している
- 優先度キューと LRU には、その場しのぎのソートではなく
container/heap/container/listを使用している