- Go言語
Goスライスとmapの使い方早見表|append・len・cap・存在チェック
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つです。
appendの結果は必ず受け取る。s = append(s, v)の形にします。領域が作り直されると、戻り値だけが新しい領域を指すためです。- 件数が見えているなら
make([]T, 0, n)で容量を先に取る。 確保と写しの回数が減ります。 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期連続の黒字決算。