mcpField
G
go-data-structures

go-data-structures

github.com/eduardo-sl開発86113 回インストール3 週間前に更新
リポジトリ

go-data-structures について

Go 開発者向けに、スライス、マップ、配列などの選択と使い方をまとめたスキルです。エイリアス、nil 値、反復処理、メモリ保持に関する不具合を避けるための指針を示し、詳細なコンテナ資料は必要に応じて参照できます。

主な機能

  1. 01nil と空スライスの指針
  2. 02append のエイリアスとコピー規則
  3. 03スライスとマップの事前割り当て
  4. 04マップ、セット、配列の選択
  5. 05コンテナとメモリの挙動
  6. 06確認チェックリスト

活用シーン

  1. 01Go コードのスライスエイリアス確認
  2. 02キューやキャッシュのデータ構造選択
  3. 03サイズが既知の場合の割り当て削減
  4. 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/v2 API では、デフォルトで 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。

検証チェックリスト

  1. nil スライスと空スライスを区別するロジックがなく、空かどうかの判定には len() を使用している
  2. 契約で [] が必要な場合、JSON に渡すスライスを明示的に空(nil ではない)にしている
  3. 関数が所有していないスライスに append していない。コピーを明示的に行っている
  4. slices.Clone/copy を使わず、大きな配列のサブスライスを長期間保持していない
  5. サイズが分かっている場合、スライスとマップを容量を指定して事前確保している
  6. 「存在しない」こととゼロ値が異なる場合は、必ず comma-ok を使用している
  7. nil の可能性があるマップに書き込んでいない
  8. 出力パスを決定的にする場合、反復の前にマップのキーをソートしている
  9. セットは map[K]struct{} として構築している(読みやすさを優先する場合は map[K]bool)
  10. 値セマンティクスまたは比較可能性が目的の場合に限り、配列を使用している
  11. 大幅に縮小した長期間保持するマップは、要素を削除するだけでなく再構築している
  12. 優先度キューと LRU には、その場しのぎのソートではなく container/heap/container/list を使用している

コメント