Goのスライスとは「長さを変えられる並び」、mapとは「キーから値を引ける対応表」です。 スライスは append で要素を足し、len で要素数、cap で確保済みの容量を調べます。mapは v, ok := m[k] でキーがあるかを確かめ、delete(m, k) で消します。この2つを押さえると、Goで書くデータ処理の大半を組み立てられます。

この記事では、スライスとmapの操作を「やりたいこと→コード→実行結果」の早見表にまとめ、そのあとで1つずつ動くコードと出力を載せます。出力はすべて go1.27.1(linux/amd64)で go run した結果です。append で cap が伸びる様子、nil mapに代入したときのpanicの文面、Go 1.21で入った slices・maps パッケージと組み込み関数 clear まで、1ページで確認できます。

スライスとmapの早見表

まずは逆引きです。「結果」の列は、後半に載せた完成形コードを実際に動かして確かめた内容です。

スライスの早見表

前提:a := []string{"りんご", "みかん", "ぶどう"}、n := []int{5, 2, 8, 2, 9, 1}

やりたいこと コード 結果
値を並べて作る a := []string{"りんご", "みかん", "ぶどう"} len=3 cap=3
長さを決めて作る make([]int, 3) [0 0 0] len=3 cap=3
容量だけ確保して作る make([]int, 0, 5) [] len=0 cap=5
末尾に1つ足す a = append(a, "もも") 4要素になる
別のスライスをつなぐ a = append(a, b...) bの全要素が末尾に入る
要素数を調べる len(a) 3
容量を調べる cap(a) 3
先頭・末尾を取る a[0]、a[len(a)-1] りんご、ぶどう
一部を切り出す a[1:3] [みかん ぶどう]
含まれるか調べる slices.Contains(a, "もも") true または false
位置を調べる slices.Index(a, "すいか") 無いときは -1
途中に差し込む a = slices.Insert(a, 1, "いちご") 1番目に入る
範囲を消す a = slices.Delete(a, 2, 4) 2番目と3番目が消える
条件で消す a = slices.DeleteFunc(a, f) fがtrueの要素が消える
昇順に並べる slices.Sort(n) [1 2 2 5 8 9]
重複をまとめる(並べ替え後) n = slices.Compact(n) [1 2 5 8 9]
逆順にする slices.Reverse(n) [9 8 5 2 1]
最大・最小 slices.Max(n)、slices.Min(n) 9、1
独立したコピーを作る dst := slices.Clone(src) dstを変えてもsrcはそのまま
中身が同じか比べる slices.Equal(x, y) true または false
全要素をゼロ値にする clear(s) [0 0 0](lenは変わらない)
長さを0にする s = s[:0] [] len=0(capは残る)

mapの早見表

前提:stock := map[string]int{"ペン": 12, "ノート": 0, "付箋": 30}

やりたいこと コード 結果
値を入れて作る map[string]int{"ペン": 12} そのまま使える
空で作る make(map[string]int) len=0、代入できる
追加・上書き stock["はさみ"] = 4 キーが無ければ追加、あれば上書き
値を取る stock["ペン"] 12
無いキーを取る stock["定規"] 0(値の型のゼロ値)
キーがあるか調べる v, ok := stock["ノート"] v=0 ok=true
無いキーを調べる v, ok := stock["定規"] v=0 ok=false
キーを消す delete(stock, "付箋") 無いキーでも何も起きない
件数を調べる len(stock) 3
全部回す for k, v := range stock 順番は毎回変わりうる
キー順に回す slices.Sorted(maps.Keys(stock)) 並べ替えたキーのスライス
独立したコピーを作る cp := maps.Clone(stock) cpを変えてもstockはそのまま
別のmapを取り込む maps.Copy(dst, src) 同じキーはsrcの値で上書き
条件で消す maps.DeleteFunc(m, f) fがtrueの組が消える
中身が同じか比べる maps.Equal(m1, m2) true または false
数を数える count[w]++ 無いキーは0から始まる
全部消す clear(stock) map[] len=0

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Goスライスとmapの使い方早見表|append・len・cap・存在チェック」

この記事の前提と動かし方

コードはどれも1ファイルで完結していて、main.go に貼って go run main.go で動きます。 外部モジュールは使わず、標準ライブラリだけです。

項目 内容
実行したGo go1.27.1 linux/amd64
実行コマンド go run main.go
必要な最小バージョン slices・maps・clear は Go 1.21 以降、maps.Keys と slices.Sorted は Go 1.23 以降
出力の扱い 出力はそのまま掲載。panicのスタックトレースに出るファイルのパスだけ /home/taro/sample/ に置き換えています

slices・maps パッケージと clear が Go 1.21 で入ったこと、maps.Keys・maps.Values・slices.Sorted が Go 1.23 で入ったことは、Go 1.21 のリリースノートと pkg.go.dev の各パッケージのページを今回開いて確かめました。

スライスの基本操作をまとめて動かす

スライスは「作る・足す・取り出す・探す・差し込む/消す・並べる・コピーする・空にする」の8つで整理できます。 下のコードは早見表の元になった完成形です。

package main

import (
    "fmt"
    "slices"
)

func main() {
    // 作る
    a := []string{"りんご", "みかん", "ぶどう"}
    b := make([]int, 3)
    c := make([]int, 0, 5)
    fmt.Println(a, len(a), cap(a))
    fmt.Println(b, len(b), cap(b))
    fmt.Println(c, len(c), cap(c))

    // 追加・連結
    a = append(a, "もも")
    a = append(a, []string{"なし", "かき"}...)
    fmt.Println(a)

    // 取り出す・切り出す
    fmt.Println(a[0], a[len(a)-1])
    fmt.Println(a[1:3], a[:2], a[4:])

    // 探す
    fmt.Println(slices.Contains(a, "もも"), slices.Index(a, "もも"), slices.Index(a, "すいか"))
    fmt.Println(slices.IndexFunc(a, func(v string) bool { return v[0:3] == "な" }))

    // 挿入・削除
    a = slices.Insert(a, 1, "いちご")
    fmt.Println(a)
    a = slices.Delete(a, 2, 4)
    fmt.Println(a)
    a = slices.DeleteFunc(a, func(v string) bool { return v == "かき" })
    fmt.Println(a)

    // 並べ替え
    n := []int{5, 2, 8, 2, 9, 1}
    slices.Sort(n)
    fmt.Println(n)
    idx, found := slices.BinarySearch(n, 8)
    fmt.Println(idx, found)
    n = slices.Compact(n)
    fmt.Println(n)
    slices.Reverse(n)
    fmt.Println(n)
    fmt.Println(slices.Max(n), slices.Min(n))
    slices.SortFunc(a, func(x, y string) int { return len(x) - len(y) })
    fmt.Println(a)

    // コピー・比較
    src := []int{1, 2, 3}
    dst := slices.Clone(src)
    dst[0] = 100
    fmt.Println(src, dst, slices.Equal(src, dst))
    buf := make([]int, 2)
    copied := copy(buf, src)
    fmt.Println(buf, copied)

    // 空にする
    clear(src)
    fmt.Println(src, len(src))
    src = src[:0]
    fmt.Println(src, len(src), cap(src))
}

実行結果:

[りんご みかん ぶどう] 3 3
[0 0 0] 3 3
[] 0 5
[りんご みかん ぶどう もも なし かき]
りんご かき
[みかん ぶどう] [りんご みかん] [なし かき]
true 3 -1
4
[りんご いちご みかん ぶどう もも なし かき]
[りんご いちご もも なし かき]
[りんご いちご もも なし]
[1 2 2 5 8 9]
4 true
[1 2 5 8 9]
[9 8 5 2 1]
9 1
[もも なし りんご いちご]
[1 2 3] [100 2 3] false
[1 2] 2
[0 0 0] 3
[] 0 3

出力を上から順に読むと、次のことがわかります。

出力の行 読み取れること
1〜3行目 リテラルで作るとlenとcapは同じ。make([]int, 3) はゼロ値で埋まり、make([]int, 0, 5) は中身が空で容量だけ5
4行目 append は1つでも、... を付けたスライスでも足せる
5〜6行目 a[1:3] は1番目から3番目の手前まで。a[:2] は先頭から、a[4:] は末尾まで
7〜8行目 slices.Index は見つからないと -1。IndexFunc は条件に合う最初の位置(ここでは「なし」の4)
9〜11行目 Insert・Delete・DeleteFunc は結果を受け取り直す形で使う
12〜16行目 Sort で昇順、BinarySearch は位置と見つかったかの2つを返す。Compact は隣り合う重複をまとめる
17行目 SortFunc は比較関数を渡して好きな順に並べる(ここではバイト長の短い順)
18〜19行目 Clone したスライスは元と独立。copy は短い方の長さぶんだけ写し、写した個数を返す
20〜21行目 clear は要素をゼロ値にするだけで長さは3のまま。s[:0] は長さを0にして容量は3のまま

結果を受け取り直す関数と、その場で書き換える関数

slices パッケージの関数は、長さが変わるものは戻り値を受け取り直し、長さが変わらないものはその場で書き換えるという分かれ方をしています。

種類 関数 書き方
長さが変わる append、slices.Insert、slices.Delete、slices.DeleteFunc、slices.Compact s = slices.Delete(s, i, j) のように代入する
長さが変わらない slices.Sort、slices.SortFunc、slices.Reverse、clear slices.Sort(s) と呼ぶだけ
新しい値を返す slices.Clone、slices.Contains、slices.Index、slices.Max、slices.Equal 戻り値を使う。元のスライスは変わらない

slices.Delete と slices.Compact は、縮んだあとに余った末尾の要素をゼロ値にします(pkg.go.dev の説明に「Delete zeroes the elements」「Compact zeroes the elements between the new length and the original length」とあります)。そのため、戻り値を受け取らずに元の変数を使い続けると、末尾にゼロ値が残ったスライスを見ることになります。上のコードを書く途中で n = slices.Compact(n) の代入を省いて試したところ、slices.Min(n) が 0 を返しました。代入を付ければ 1 が返ります。

appendでlenとcapが伸びる様子を見る

len は「いま入っている要素数」、cap は「作り直さずに入れられる上限」です。 append は cap に空きがあればそこに書き込み、空きがなければより大きい領域を新しく確保して中身を写します。

package main

import "fmt"

func main() {
    var s []int
    fmt.Printf("len=%d cap=%d nil=%t\n", len(s), cap(s), s == nil)
    prev := cap(s)
    for i := 1; i <= 20; i++ {
        s = append(s, i)
        if cap(s) != prev {
            fmt.Printf("len=%2d cap=%2d  <- capが %d から %d に伸びた\n", len(s), cap(s), prev, cap(s))
            prev = cap(s)
        }
    }

    t := make([]int, 0, 20)
    before := cap(t)
    for i := 1; i <= 20; i++ {
        t = append(t, i)
    }
    fmt.Printf("make([]int, 0, 20): len=%d cap=%d (capの変化: %t)\n", len(t), cap(t), before != cap(t))
}

実行結果:

len=0 cap=0 nil=true
len= 1 cap= 4  <- capが 0 から 4 に伸びた
len= 5 cap= 8  <- capが 4 から 8 に伸びた
len= 9 cap=16  <- capが 8 から 16 に伸びた
len=17 cap=32  <- capが 16 から 32 に伸びた
make([]int, 0, 20): len=20 cap=20 (capの変化: false)

20個足すあいだに、容量の確保は4回起きました。一方、最初に make([]int, 0, 20) で容量を確保したスライスは、20個足しても cap が変わっていません。

flowchart LR
    A["append(s, v)"] --> B{"len < cap ?"}
    B -- はい --> C["空いている場所に書く<br>len が1増える"]
    B -- いいえ --> D["大きい領域を新しく確保"]
    D --> E["中身を写して末尾に書く<br>len と cap が増える"]
用語 意味 調べ方
len 入っている要素の数。添字で触れるのは 0 から len-1 まで len(s)
cap 先頭から数えて、確保済みの領域に入る数 cap(s)
nil スライス var s []int の状態。len=0 cap=0 で、そのまま append できる s == nil

ここで覚えておきたい点は3つです。

  1. append の結果は必ず受け取る。 s = append(s, v) の形にします。領域が作り直されると、戻り値だけが新しい領域を指すためです。
  2. 件数が見えているなら make([]T, 0, n) で容量を先に取る。 確保と写しの回数が減ります。
  3. cap の伸び方の数値に頼らない。 上の「0→4→8→16→32」は go1.27.1 で []int を使ったときの結果で、Goのバージョンや要素の型によって変わります。「足りなくなったら大きめに取り直す」という動きだけを前提にしておくと、どの版でも通用します。

スライスの共有でつまずきやすい点

スライスは「配列の一部を指す窓」なので、切り出したスライスと元のスライスは同じ領域を見ています。 これを知っておくと、思いがけない書き換わりを避けられます。

package main

import "fmt"

func main() {
    base := []int{1, 2, 3, 4, 5}
    head := base[:2]
    fmt.Println(head, len(head), cap(head))

    head = append(head, 99)
    fmt.Println("base:", base)
    fmt.Println("head:", head)

    base2 := []int{1, 2, 3, 4, 5}
    safe := base2[:2:2]
    fmt.Println(safe, len(safe), cap(safe))
    safe = append(safe, 99)
    fmt.Println("base2:", base2)
    fmt.Println("safe: ", safe)

    // 関数に渡す
    nums := []int{1, 2, 3}
    double(nums)
    fmt.Println(nums)
    add(nums)
    fmt.Println(nums)
    nums = addRet(nums)
    fmt.Println(nums)

    // nil と空
    var n []int
    e := []int{}
    fmt.Println(n == nil, e == nil, len(n), len(e))
    n = append(n, 1)
    fmt.Println(n)

    // 2次元
    grid := make([][]int, 2)
    for i := range grid {
        grid[i] = make([]int, 3)
    }
    grid[1][2] = 7
    fmt.Println(grid)

    // range
    for i, v := range []string{"a", "b"} {
        fmt.Println(i, v)
    }
    for i := range 3 {
        fmt.Print(i, " ")
    }
    fmt.Println()
}

func double(s []int) {
    for i := range s {
        s[i] *= 2
    }
}
func add(s []int)          { s = append(s, 100) }
func addRet(s []int) []int { return append(s, 100) }

実行結果:

[1 2] 2 5
base: [1 2 99 4 5]
head: [1 2 99]
[1 2] 2 2
base2: [1 2 3 4 5]
safe:  [1 2 99]
[2 4 6]
[2 4 6]
[2 4 6 100]
true false 0 0
[1]
[[0 0 0] [0 0 7]]
0 a
1 b
0 1 2 

切り出したスライスへのappendが元を書き換える

head := base[:2] は len=2 ですが、cap は元の5を引き継ぎます。空きがあるので、append(head, 99) は base の3番目の場所に 99 を書き込みます。出力の base: [1 2 99 4 5] がその結果です。

直し方は、切り出すときに容量の上限も指定する base[:2:2](3つ目の数字が容量の終わり) です。cap が2になるので、append は新しい領域を確保し、base2 は [1 2 3 4 5] のまま残ります。slices.Clone(base[:2]) で独立したコピーを作る方法でも同じ結果になります。

書き方 lenとcap appendしたとき
base[:2] len=2 cap=5 元の領域に書き込む(元の3番目が変わる)
base[:2:2] len=2 cap=2 新しい領域を確保する(元は変わらない)
slices.Clone(base[:2]) len=2 cap=2 最初から別の領域

関数に渡したときに変わるもの・変わらないもの

出力の [2 4 6]→[2 4 6]→[2 4 6 100] の3行が、関数に渡したときの動きです。

関数の中でしたこと 呼び出し元から見えるか 理由
要素の書き換え(s[i] *= 2) 見える 同じ領域を指しているため
append して戻さない 見えない 呼び出し元の len は変わらないため
append して戻り値を返す 見える 呼び出し元が nums = addRet(nums) で受け取るため

要素を足す関数は、スライスを戻り値で返す形にすると覚えておくと迷いません。

nilスライスと空スライス

var n []int は nil、e := []int{} は nil ではない空スライスです。どちらも len は 0 で、append も range もそのまま使えます。空かどうかは len(s) == 0 で判定すると、両方をまとめて扱えます。

範囲外の添字はpanicになる

len 以上の添字で読むと、実行時に止まります。

package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    fmt.Println(s[5-2])
}

実行結果:

panic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
    /home/taro/sample/main.go:7 +0x9
exit status 2

index out of range [3] with length 3 は「長さ3のスライスに添字3で触れた」という意味です。添字を計算で出すときは、先に if i < len(s) で確かめます。

mapの基本操作をまとめて動かす

mapは「作る・入れる・取る・あるか調べる・消す・回す・コピーする・空にする」で整理できます。

package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {
    // 作る
    stock := map[string]int{"ペン": 12, "ノート": 0, "付箋": 30}
    m2 := make(map[string]int)
    fmt.Println(stock, len(stock), len(m2))

    // 追加・更新・取得
    stock["はさみ"] = 4
    stock["ペン"] += 3
    fmt.Println(stock["ペン"], stock["定規"])

    // 存在チェック
    v, ok := stock["ノート"]
    fmt.Println(v, ok)
    v, ok = stock["定規"]
    fmt.Println(v, ok)
    if n, ok := stock["付箋"]; ok {
        fmt.Println("付箋の在庫:", n)
    }

    // 削除
    delete(stock, "付箋")
    delete(stock, "存在しないキー")
    fmt.Println(stock, len(stock))

    // キー順に回す
    for _, k := range slices.Sorted(maps.Keys(stock)) {
        fmt.Println(k, stock[k])
    }
    fmt.Println(slices.Sorted(maps.Values(stock)))

    // コピー・比較
    cp := maps.Clone(stock)
    cp["ペン"] = 999
    fmt.Println(stock["ペン"], cp["ペン"], maps.Equal(stock, cp))
    alias := stock
    alias["ペン"] = 1
    fmt.Println(stock["ペン"])

    // 条件で削除・マージ
    maps.DeleteFunc(cp, func(k string, v int) bool { return v == 0 })
    fmt.Println(cp)
    maps.Copy(cp, map[string]int{"消しゴム": 7, "ペン": 20})
    fmt.Println(cp)

    // 集計
    words := []string{"go", "map", "go", "slice", "go", "map"}
    count := map[string]int{}
    for _, w := range words {
        count[w]++
    }
    fmt.Println(count)
    group := map[int][]string{}
    for _, w := range words {
        group[len(w)] = append(group[len(w)], w)
    }
    fmt.Println(group)

    // 集合
    seen := map[string]struct{}{}
    for _, w := range words {
        seen[w] = struct{}{}
    }
    fmt.Println(len(seen), slices.Sorted(maps.Keys(seen)))

    // 空にする
    clear(stock)
    fmt.Println(stock, len(stock))
}

実行結果:

map[ノート:0 ペン:12 付箋:30] 3 0
15 0
0 true
0 false
付箋の在庫: 30
map[はさみ:4 ノート:0 ペン:15] 3
はさみ 4
ノート 0
ペン 15
[0 4 15]
15 999 false
1
map[はさみ:4 ペン:999]
map[はさみ:4 ペン:20 消しゴム:7]
map[go:3 map:2 slice:1]
map[2:[go go go] 3:[map map] 5:[slice]]
3 [go map slice]
map[] 0
出力の行 読み取れること
1行目 fmt.Println はmapをキーの順に並べて表示する(表示のための並べ替え)
2行目 += で更新できる。無いキー 定規 を読むと 0 が返る
3〜5行目 v, ok で「値が0で存在する」と「存在しない」を区別できる
6行目 delete は無いキーを指定しても止まらない
7〜10行目 slices.Sorted(maps.Keys(m)) でキー順に回せる。maps.Values で値だけ取り出せる
11〜12行目 maps.Clone は独立したコピー。alias := stock は同じmapを指すので、片方の変更がもう片方に見える
13〜14行目 maps.DeleteFunc で値が0の組を消し、maps.Copy で別のmapを取り込む(同じキーは上書き)
15〜17行目 数える・グループに分ける・重複を除く、の定番3パターン
18行目 clear で全部の組が消える

存在チェックは v, ok 形式で書く

mapは、無いキーを読んでもpanicにならず、値の型のゼロ値を返します。 int なら 0、string なら空文字、bool なら false です。在庫が0の「ノート」と、登録されていない「定規」は、stock[k] だけではどちらも 0 に見えます。

そこで使うのが2つ目の戻り値です。

v, ok := stock["ノート"] // v=0, ok=true  → 登録済みで在庫0
v, ok = stock["定規"]    // v=0, ok=false → 未登録
書き方 向いている場面
v := m[k] 無ければゼロ値でよいとき(カウント、合計など)
v, ok := m[k] 「無い」と「ゼロ値」を区別したいとき
if v, ok := m[k]; ok { ... } あるときだけ処理したいとき。v と ok の範囲が if の中に収まる
_, ok := m[k] あるかどうかだけ知りたいとき

mapを回す順番は決まっていない

for k, v := range m の順番は、Goの仕様として決められていません。同じプログラムでも実行のたびに変わりえます。順番が必要なときは、キーを取り出して並べ替えてから回します。 Go 1.23 以降は slices.Sorted(maps.Keys(m)) の1行で書けます。Go 1.21〜1.22 では、for k := range m でキーをスライスに集めてから slices.Sort を呼びます。

一方、fmt.Println(m) の表示はキー順にそろえて出力されます。表示がそろっているからといって、range の順番もそろうわけではない点を押さえておきます。

数える・分ける・重複を除く

無いキーがゼロ値を返す性質を使うと、初期化の分岐を書かずに集計できます。

パターン コード 仕組み
数える count[w]++ 無いキーは 0 から始まる
グループに分ける group[k] = append(group[k], w) 無いキーは nil スライスで、nil に append できる
重複を除く(集合) seen[w] = struct{}{} 値に空の構造体を使うと、キーだけを持つ集合になる

nil mapへの代入はpanicになる

var m map[string]int と宣言しただけのmapは nil で、読むことはできても代入はできません。 エラー文ごと再現します。

package main

import "fmt"

func main() {
    var m map[string]int
    fmt.Println(m == nil, len(m), m["a"])
    _, ok := m["a"]
    fmt.Println(ok)
    delete(m, "a")
    fmt.Println("ここまでは動く")
    m["a"] = 1
    fmt.Println("ここには来ない")
}

実行結果:

true 0 0
false
ここまでは動く
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
    /home/taro/sample/main.go:12 +0x176
exit status 2

読み取り、v, ok 形式、delete は nil map でもそのまま動き、代入の行で panic: assignment to entry in nil map が出て止まります。 スライスは nil のまま append できるので、同じ感覚でmapを扱うとここで止まります。

直し方は、使う前に作ることです。

// 直す前
var m map[string]int
m["a"] = 1 // panic: assignment to entry in nil map

// 直した後(どちらでもよい)
m := map[string]int{}
m := make(map[string]int)

構造体のフィールドにmapを持たせる場合も同じで、構造体のゼロ値ではフィールドのmapが nil です。コンストラクタ関数の中で make しておくか、使う直前に if m == nil { m = make(...) } と確かめます。

操作 nil スライス nil map
len 0 0
読む 範囲外なのでpanic ゼロ値が返る
range 0回まわる 0回まわる
追加 append で足せる 代入するとpanic
delete (対象外) 何も起きない

コンパイル時に教えてくれるエラー

実行する前に、コンパイラが知らせてくれるものもあります。

package main

import "fmt"

func main() {
    m := map[string][]int{"a": {1}}
    type P struct{ X int }
    pm := map[string]P{"a": {1}}
    pm["a"].X = 2
    fmt.Println(m, pm)
    s := []int{1}
    var k map[[]int]string
    _ = s
    _ = k
}

実行結果(コンパイルエラー):

# command-line-arguments
./main.go:9:2: cannot assign to struct field pm["a"].X in map
./main.go:12:12: invalid map key type []int
エラー文 意味 直し方
cannot assign to struct field pm["a"].X in map mapの値が構造体のとき、フィールドだけを直接書き換えることはできない いったん取り出して変更し、pm["a"] = p で入れ直す。または値をポインタ(map[string]*P)にする
invalid map key type []int スライスは == で比べられないので、キーにできない 文字列や配列、比較できる構造体をキーにする

mapのキーにできるのは、== で比べられる型です。数値、文字列、bool、ポインタ、配列、そしてそれらだけで構成された構造体が使えます。スライス、map、関数はキーにできません。

Go 1.21以降のslices・mapsパッケージとclear

Go 1.21 で、スライスとmapの定番処理が標準ライブラリに入りました。 それ以前は for 文で書いていた処理が1行になります。

slicesパッケージの主な関数

関数 役割 入った版
slices.Contains(s, v) 含まれるか 1.21
slices.Index(s, v) / IndexFunc 最初の位置(無ければ -1) 1.21
slices.Insert(s, i, v...) i番目に差し込む 1.21
slices.Delete(s, i, j) / DeleteFunc 範囲または条件で消す 1.21
slices.Sort(s) / SortFunc 並べ替え 1.21
slices.BinarySearch(s, v) 並べ替え済みのスライスから探す 1.21
slices.Compact(s) 隣り合う重複をまとめる 1.21
slices.Reverse(s) 逆順にする 1.21
slices.Max(s) / Min 最大・最小(空のスライスではpanic) 1.21
slices.Clone(s) / Equal コピー・比較 1.21
slices.Concat(s1, s2...) 複数のスライスをつなぐ 1.22
slices.Sorted(seq) / Collect イテレータからスライスを作る 1.23

mapsパッケージの主な関数

関数 役割 入った版
maps.Clone(m) コピーを作る 1.21
maps.Copy(dst, src) srcの組をdstに入れる 1.21
maps.DeleteFunc(m, f) 条件で消す 1.21
maps.Equal(m1, m2) / EqualFunc 比較 1.21
maps.Keys(m) / Values キーまたは値のイテレータ 1.23
maps.All(m) / Collect / Insert イテレータとの相互変換 1.23

maps.Keys が返すのはスライスではなくイテレータ(iter.Seq)です。スライスにしたいときは slices.Collect(maps.Keys(m))、並べ替えまでしたいときは slices.Sorted(maps.Keys(m)) と組み合わせます。

clearの動きはスライスとmapで違う

対象 clear のあと 実行結果
スライス 全要素がゼロ値になる。lenとcapはそのまま [0 0 0] len=3
map 全部の組が消える map[] len=0

スライスを「0件」にしたいときは s = s[:0](容量を残して再利用)か s = nil(領域ごと手放す)を使います。clear は「同じ長さのまま中身を初期化する」ための関数です。

以前の書き方との対応

Go 1.20 以前のコードを読むときのために、対応を並べておきます。

やりたいこと 以前の書き方 Go 1.21以降
含まれるか for で回して比べる slices.Contains(s, v)
i番目を消す append(s[:i], s[i+1:]...) slices.Delete(s, i, i+1)
並べ替え sort.Ints(s)、sort.Slice(s, less) slices.Sort(s)、slices.SortFunc(s, cmp)
スライスのコピー append([]int(nil), s...) slices.Clone(s)
mapを空にする for k := range m { delete(m, k) } clear(m)
mapのコピー for で1つずつ代入 maps.Clone(m)

スライスとmapの選び方

順番と位置で扱うならスライス、名前や番号で引くならmapが基本です。

判断のポイント スライス map
取り出し方 位置(0, 1, 2…) キー(文字列、数値など)
順番 保たれる 決まっていない
重複 持てる キーは重複しない
特定の値を探す 先頭から順に調べる キーで直接引ける
ゼロ値(nil)のまま足せるか append で足せる 代入の前に make が必要
向いている用途 一覧、履歴、並べ替えて表示するもの ID引き、集計、重複の除去

両方を組み合わせる場面も多くあります。たとえば「表示は登録順、検索はIDで」というときは、スライスで順番を持ち、mapで ID から位置や要素を引く形にします。

つまずきやすい点のチェックリスト

確認すること そうなる理由 対応
append の戻り値を受け取っているか 領域が作り直されると戻り値だけが新しい領域を指す s = append(s, v)
slices.Delete や Compact の戻り値を受け取っているか 余った末尾はゼロ値になる s = slices.Delete(s, i, j)
切り出したスライスに append していないか cap に空きがあると元を書き換える s[i:j:j] か slices.Clone
mapに代入する前に作っているか nil map への代入はpanic make(map[K]V) か map[K]V{}
「無い」と「ゼロ値」を区別しているか 無いキーはゼロ値を返す v, ok := m[k]
mapの順番に頼っていないか range の順番は決まっていない slices.Sorted(maps.Keys(m))
添字が len 未満か 範囲外はpanic if i < len(s)
mapを別の変数に代入してコピーしたつもりになっていないか 同じmapを指す maps.Clone(m)

よくある質問

Q. lenとcapはどう使い分けますか? A. ふだんの処理で使うのは len です。ループの回数や空かどうかの判定は len で行います。cap は、make([]T, 0, n) で先に容量を取るときと、切り出したスライスに append して元が変わらないかを確かめるときに使います。

Q. mapにキーがあるかどうかだけを調べるには? A. _, ok := m[k] と書きます。1つ目の戻り値をアンダースコアで受けて捨て、ok だけを使います。if _, ok := m[k]; ok { ... } の形にすると、1行で分岐まで書けます。

Q. スライスから要素を1つ削除するには? A. Go 1.21 以降は s = slices.Delete(s, i, i+1) です。条件で消すなら s = slices.DeleteFunc(s, func(v T) bool { ... }) を使います。どちらも戻り値を受け取り直します。

Q. mapを回している途中で delete してもよいですか? A. range の途中でそのmapのキーを delete することは、Goの言語仕様で認められた使い方として一般に知られています(消した組はそのあと出てきません)。ただ、条件で消すだけなら maps.DeleteFunc を使うと意図が1行で伝わります。この動きは今回のコードでは maps.DeleteFunc の形で確かめています。

まとめ

  • スライスは append の結果を必ず受け取る(s = append(s, v))。len は要素数、cap は確保済みの容量
  • cap が足りなくなると大きい領域が確保される。件数がわかるなら make([]T, 0, n) で先に取る
  • 切り出したスライスは元と領域を共有する。独立させるなら s[i:j:j] か slices.Clone
  • mapの存在チェックは v, ok := m[k]。無いキーはゼロ値を返すので、ok で区別する
  • nil map は読めるが代入できない(panic: assignment to entry in nil map)。使う前に make する
  • Go 1.21 以降は slices・maps・clear を使う。clear はスライスではゼロ値で埋め、mapでは全部消す
  • 順番が必要ならスライス、キーで引くならmap。mapをキー順に回すなら slices.Sorted(maps.Keys(m))

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Goスライスとmapの使い方早見表|append・len・cap・存在チェック」

株式会社RJCでは、Goのスライスとmapの使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?