Go slices/mapsパッケージ実践—自前ループを標準関数に置き換える

Go slices/mapsパッケージ実践—自前ループを標準関数に置き換える | mohablog

重複の削除、要素の存在チェック、構造体スライスのソート。Goでこのあたりを書くたびに、似たような for ループを毎回書いていましたよね。Go 1.21 で標準入りした slicesmaps パッケージは、その大半を1行に畳めます。

現行の Go 1.26.4(2026年6月リリース)までに、検索・ソート・削除・重複除去・マップ操作がジェネリック関数として出揃いました。sort.Slice に渡していた比較クロージャや、自前の contains ヘルパーは、もう書かなくて済みます。

目次

標準パッケージに寄せると何が減るのか

Go 1.18のジェネリクス導入前は、汎用のスライス操作を書こうとすると interface{} とリフレクションに頼るしかありませんでした。sort.Slice がその代表です。比較関数をインデックスで書くため、要素そのものを触れません。

// Go 1.20以前: インデックス越しの比較
sort.Slice(users, func(i, j int) bool {
    return users[i].age < users[j].age
})

slices は型パラメータで要素を直接受け取ります。比較対象が users[i] ではなく a, b になり、コンパイル時に型が決まる。実行時リフレクションが消えるぶん、誤った型を渡すミスはビルドで止まります。

  • 検索: Contains / Index / BinarySearch
  • ソート: Sort / SortFunc / SortStableFunc
  • 編集: Delete / Insert / Compact / Replace
  • マップ: maps.Clone / maps.Equal / maps.Keys

存在チェックと検索はContainsから

ContainsとIndexで自前ヘルパーを捨てる

「このスライスに値が入っているか」だけのために、自前の contains ヘルパーを書いていたコードは多い。slices.Contains がそのまま置き換えます。

package main

import (
    "fmt"
    "slices"
)

func main() {
    langs := []string{"Go", "Python", "Rust"}
    fmt.Println(slices.Contains(langs, "Go"))
    fmt.Println(slices.Index(langs, "Python"))
    fmt.Println(slices.Index(langs, "TypeScript"))
}

実行結果。Index は見つからなければ -1 を返します。

true
1
-1

条件で探したいときは ContainsFuncIndexFunc。述語を渡すと、最初に true を返した要素で判定します。

nums := []int{3, 7, 12, 5}
hasEven := slices.ContainsFunc(nums, func(n int) bool {
    return n%2 == 0
})
fmt.Println(hasEven) // 12が該当
true

ソート済みならBinarySearch

BinarySearch は昇順ソート済みのスライスが前提。インデックスと、見つかったかの真偽値を返します。見つからない場合は「挿入すべき位置」が返るので、ソート順を保ったまま追加できます。

sorted := []int{2, 4, 6, 8, 10}
i, found := slices.BinarySearch(sorted, 6)
fmt.Println(i, found)

i, found = slices.BinarySearch(sorted, 7)
fmt.Println(i, found) // 7は未登録、挿入位置は3
2 true
3 false

ソートはSortとSortFuncで書き分ける

比較関数はcmp.Compareに寄せる

数値や文字列など cmp.Ordered を満たす型は slices.Sort だけで昇順に並びます。構造体や独自順序は SortFunc に比較関数を渡す。比較関数は a<b で負、a>b で正、等しければ 0 を返す約束です。標準の cmp.Compare に委譲すると自前の三項分岐が要りません。

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type user struct {
    name string
    age  int
}

func main() {
    users := []user{
        {"Sato", 32},
        {"Ito", 28},
        {"Kato", 41},
    }
    slices.SortFunc(users, func(a, b user) int {
        return cmp.Compare(a.age, b.age)
    })
    fmt.Println(users)
}
[{Ito 28} {Sato 32} {Kato 41}]

安定ソートが要るとき

同値の要素の元の並びを保ちたいなら SortStableFunc。たとえば「年齢で並べるが、同じ年齢は登録順のまま」という要件で効きます。3つの使い分けを整理します。

関数比較安定性使う場面
Sort型の自然順序不要(同値は区別なし)int・stringの単純な昇順
SortFunc渡した関数保証なし構造体・独自順序
SortStableFunc渡した関数保証あり同値の元順序を残したい

削除と重複除去でハマる2つの罠

このパッケージで一番事故りやすいのが DeleteCompact です。どちらも「元のスライスを書き換えて、結果を返り値で渡す」設計。返り値を無視すると壊れます。以前、削除した要素数だけ末尾にゼロ値が残ったスライスをそのままレスポンスに詰めてしまい、テストの期待値とズレて初めて気づいたことがあります。

slices.Deleteは返り値を必ず受ける

アンチパターンから。返り値を捨てると、元のスライス変数は長さが変わらないまま、末尾がゼロ化されます。

s := []int{10, 20, 30, 40, 50}
slices.Delete(s, 1, 3) // 返り値を捨てている
fmt.Println(s)
[10 40 50 0 0]

末尾の 0 0 は、Go 1.22以降の Delete が解放対象のスロットをゼロ化する仕様によるもの。ポインタを含む要素のメモリリークを防ぐ動きですが、返り値を受けないと長さ5のままゼロが見えてしまいます。正しくは返り値を受け直します。

s := []int{10, 20, 30, 40, 50}
s = slices.Delete(s, 1, 3) // インデックス1,2を削除
fmt.Println(s)
[10 40 50]

Compactは「連続」しか消さない

Compact の名前から「重複を全部消す」と読むと外します。消すのは 連続して並んだ 等値だけ。Unixの uniq と同じ挙動です。

s := []int{1, 1, 2, 2, 2, 3, 1}
s = slices.Compact(s)
fmt.Println(s) // 末尾の1は離れているので残る
[1 2 3 1]

全体から重複を消したいなら、先に Sort で同値を隣り合わせてから Compact に渡す。この2段が定番です。

s := []int{1, 1, 2, 2, 2, 3, 1}
slices.Sort(s)
s = slices.Compact(s)
fmt.Println(s)
[1 2 3]

mapsパッケージはCloneとEqualが実用的

標準の maps は Go 1.21時点では Clone / Copy / Equal / EqualFunc / DeleteFunc の5つから始まりました。手書きの for k, v := range コピーや、2マップの一致判定ループを置き換えます。

a := map[string]int{"x": 1, "y": 2}
b := map[string]int{"y": 2, "x": 1}
fmt.Println(maps.Equal(a, b)) // キー順は無関係
true

Cloneは浅いコピー

maps.Clone は新しいマップを返しますが、値のコピーは通常の代入と同じ。値がスライスやマップだと、中身の参照は共有されます。トップレベルのキー追加は独立する一方、ネストした要素を書き換えると元にも波及する点に気をつけてください。

original := map[string][]int{"x": {1, 2}}
cloned := maps.Clone(original)
cloned["x"][0] = 99 // 中のスライスは共有されている
fmt.Println(original["x"])
[99 2]

Keys/Valuesはiter.Seqを返す

マップのキー一覧を取る maps.KeysGo 1.23 で追加され、スライスではなく iter.Seq を返します。golang.org/x/exp/maps 時代は []K を返していたため、移行時は戻り値が iter.Seq に変わった箇所でコンパイルが通らなくなります。受け側は次のセクションの slices 側関数とつなぎます。

slices.SortedとCollectでイテレータをつなぐ

Go 1.23で slicesmaps が得たのは、iter.Seq を介した連結です。マップのキーをソート済みスライスにする処理は、これまで3行のループでした。

// Go 1.22以前
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

slices.Sortediter.Seq を受け取り、収集してソートまで済ませます。maps.Keys の戻り値をそのまま渡せます。

m := map[string]int{"go": 3, "py": 1, "rs": 2}
keys := slices.Sorted(maps.Keys(m))
fmt.Println(keys)

vals := slices.Sorted(maps.Values(m))
fmt.Println(vals)
[go py rs]
[1 2 3]

イテレータ自体の自作や iter.Pull の使い方は別記事のGoのイテレータを自作するで扱っています。ここで押さえたいのは、slices/maps がその仕組みの受け皿として整備された点です。

まとめ

自前で書いていたスライス・マップ操作は、slices/maps でほぼ標準関数に寄せられます。移行で押さえる勘所を整理します。

  • Contains/Index で自前の存在チェックヘルパーは不要。Index の未検出は -1
  • ソートは SortFunc + cmp.Compare。同値の順序を残すなら SortStableFunc
  • Delete/Compact は返り値を必ず受け直す。Compact が消すのは連続した重複だけ
  • maps.Clone は浅いコピー。ネストしたスライス・マップは参照を共有する
  • Go 1.23の maps.Keys/Valuesiter.Seqslices.Sorted と直結できる

Go 1.26環境では、sort.Slicegolang.org/x/exp/slices は標準パッケージで置き換えられます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次