Go基礎 #5 コレクション — array, slice, map
#4 関数、多値返却、error型 で関数とエラーを見ました。今回は — データを束ねる道具たち。Goの3つのコレクション型。
3つのコレクションを一目で #
| 長さ | 値の変更 | 一般的な使用度 | |
|---|---|---|---|
| array | 固定 | OK | ほとんど使わない |
| slice | 可変 | OK | 圧倒的によく使う |
| map | 可変 | OK | キー値に非常によく使う |
Array — 固定長 #
var nums [3]int // [0 0 0]
nums[0] = 10
nums[1] = 20
fmt.Println(nums) // [10 20 0]
primes := [5]int{2, 3, 5, 7, 11}
fmt.Println(len(primes)) // 5長さが型の一部です。[3]int と [5]int は別の型です。
var a [3]int
var b [5]int
// a = b ✗ 違う型この制約のためarrayは一般的なデータ構造としては不便です。ほぼすべての場面でスライスを使います。 arrayに直接出会うのは普通次の場面です。
- ハッシュ結果のような固定サイズ (
[32]bytefor SHA-256) - 非常に狭いパフォーマンス最適化
Slice — 可変長の真の主役 #
nums := []int{1, 2, 3, 4, 5}
fmt.Println(len(nums)) // 5
nums = append(nums, 6) // 末尾に追加
fmt.Println(nums) // [1 2 3 4 5 6][]int (角括弧の中に長さがない)がスライスの型。長さは実行時に決まり、自由に変わります。
作る — make
#
s1 := make([]int, 5) // 長さ 5, すべて zero — [0 0 0 0 0]
s2 := make([]int, 0, 10) // 長さ 0, 容量 103番目の引数は容量(capacity)。スライスの動作原理の核心です — もう少し後で説明します。
インデックスとスライシング #
s := []int{10, 20, 30, 40, 50}
s[0] // 10
s[1:3] // [20 30] — [start, end)
s[:3] // [10 20 30]
s[2:] // [30 40 50]
s[:] // 全体Python/JSと似ています。開始は含み、終了は含まない。
Sliceの動作原理 — 長さ vs 容量 #
これがGoのスライスを理解する核心です。
スライスは内部的に3つの部分で構成されます。
- ポインタ — 実際のデータ(配列)を指す
- 長さ (len) — 現在入っている要素数
- 容量 (cap) — データ空間のサイズ
s := make([]int, 3, 5)
fmt.Println(len(s), cap(s)) // 3 5
s = append(s, 100)
fmt.Println(len(s), cap(s)) // 4 5
s = append(s, 200)
fmt.Println(len(s), cap(s)) // 5 5
s = append(s, 300)
fmt.Println(len(s), cap(s)) // 6 10 ← cap 自動増加 (普通 2 倍)容量が足りなければ — append が新しい配列を確保してコピーします。普通2倍ずつ大きくなります。頻繁にappendする場面では、あらかじめcapを取っておくと効率的です。
result := make([]int, 0, 1000) // 1000 までは再割り当てなし
for i := 0; i < 1000; i++ {
result = append(result, i*i)
}Sliceの罠 — 同じ配列を共有 #
スライスはポインタ + 長さ + 容量なので、2つのスライスが同じ配列を指すことがあります。
a := []int{1, 2, 3, 4, 5}
b := a[1:4] // [2 3 4]
b[0] = 999
fmt.Println(a) // [1 999 3 4 5] ← a も変わる
fmt.Println(b) // [999 3 4]b は a の一部に対するviewです。一方で変更したことが他方にも反映されます。
これがしばしば事故を起こします。本当のコピーが必要なら — copy または append のトリック。
a := []int{1, 2, 3}
// copy
b := make([]int, len(a))
copy(b, a)
// append
c := append([]int{}, a...)
b[0] = 999
fmt.Println(a) // [1 2 3] (影響なし)よく使うスライスパターン #
s := []int{1, 2, 3, 4, 5}
// 末尾に追加
s = append(s, 6)
s = append(s, 7, 8, 9) // 複数個
// 他のスライスと結合
s2 := []int{10, 20}
s = append(s, s2...) // ... で展開
// 長さ
n := len(s)
// 空かどうか
isEmpty := len(s) == 0
// インデックス i の要素を削除 (順序維持)
i := 2
s = append(s[:i], s[i+1:]...)
// 順序無視で削除 (最後と swap 後に切り捨て)
s[i] = s[len(s)-1]
s = s[:len(s)-1]nil スライス — 空スライスとほぼ同じ
#
var s []int // nil
fmt.Println(len(s)) // 0
fmt.Println(s == nil) // true
s = append(s, 1) // OK — append は nil を受け取っても良い宣言だけで初期化していないとnil。しかし — append や len などほぼすべての動作がnilスライスでも安全です。特に気にしなくて大丈夫です。
Map — キー値 #
ages := map[string]int{
"カーティス": 30,
"アリス": 25,
}
ages["ボブ"] = 35 // 追加
fmt.Println(ages["カーティス"]) // 30
delete(ages, "アリス") // 削除
fmt.Println(len(ages)) // 2map[キー型]値型 で型を書きます。
作る — make
#
ages := make(map[string]int)
ages["カーティス"] = 30
// またはリテラル
empty := map[string]int{}キー存在確認 — comma-ok #
最もよく使うパターン。
ages := map[string]int{"カーティス": 30}
// 単純アクセス — なければ zero value
fmt.Println(ages["存在しないキー"]) // 0 (混乱の可能性)
// comma-ok パターン — 存在を確認
if age, ok := ages["カーティス"]; ok {
fmt.Println("あり:", age)
} else {
fmt.Println("なし")
}map[キー] は2つの値を返すことができます — 値と存在の有無。値自体がzero valueなら単純アクセスでは区別できません。comma-okが標準です。
巡回 — 順序なし #
for name, age := range ages {
fmt.Println(name, age)
}Goのmap巡回は毎回違う順序です。意図的にrandomizeされます(コードが順序に依存しないよう強制)。ソートされた順序が必要なら、キーをスライスに集めてソートします。
import "sort"
keys := make([]string, 0, len(ages))
for k := range ages {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, ages[k])
}nil map — 罠に注意
#
var m map[string]int だけ宣言するとnil mapで — 読み取りはOKだが書き込みはpanicします。
var m map[string]int
fmt.Println(m["キー"]) // 0 (読み取り OK)
fmt.Println(len(m)) // 0 (OK)
m["キー"] = 1 // ✗ panic: assignment to entry in nil map書き込み前に必ず make またはリテラルで初期化します。スライスのnil動作とは違う点です。
よく使うmapパターン #
counts := make(map[string]int)
words := []string{"a", "b", "a", "c", "b", "a"}
for _, w := range words {
counts[w]++ // 初回は zero (0) から +1
}
fmt.Println(counts) // map[a:3 b:2 c:1]キーがないとき自動でzero valueになる点がカウンティングに自然です。
Setを作る — map[T]struct #
Goにはset型がないので — 空のstructを値にしたmapで作ります。
seen := make(map[string]struct{})
seen["a"] = struct{}{}
seen["b"] = struct{}{}
if _, ok := seen["a"]; ok {
fmt.Println("すでにあり")
}空のstructはメモリを0バイト占めます(なのでsetの用途で標準)。bool を値にしても動作は同じですが、メモリ面で空のstructの方が正確です。
コレクションを関数に渡すとき #
スライスは参照セマンティクス #
func modify(s []int) {
s[0] = 999 // 呼び出し側にも見える
}
nums := []int{1, 2, 3}
modify(nums)
fmt.Println(nums) // [999 2 3]スライス自体(ヘッダ)は値でコピーされますが、中のポインタが同じ配列を指します。データの変更は呼び出し側に見えます。
ただし append の結果は違うことがあります — appendが新しい配列を確保すると、元のスライスヘッダは変わりません。
func appendBad(s []int) {
s = append(s, 100) // 新しい配列の可能性 — 呼び出し側に見えない
}
nums := []int{1, 2, 3}
appendBad(nums)
fmt.Println(nums) // [1 2 3] — 追加されない
// 推奨 — 結果を返す
func appendGood(s []int) []int {
return append(s, 100)
}
nums = appendGood(nums)mapは参照セマンティクス #
func modify(m map[string]int) {
m["new"] = 100 // 呼び出し側にも見える
}mapはスライスよりさらにシンプルに参照セマンティクスです。関数内の変更が常に見えます。
まとめ #
今回の記事で整理した内容:
- array — 固定長、ほとんど使わない
- slice — 可変、ほぼすべてのコレクション用途
- スライスはポインタ+長さ+容量。capが足りなければ新しい配列を確保
- 同じ配列共有の罠 —
copyまたはappend([]T{}, ...)でコピー - nilスライスは安全、nil mapは書き込み時にpanic
- map —
map[キー]値、comma-okで存在確認 - map巡回は毎回違う順序
- setは
map[T]struct{}で作る - スライス/mapは参照セマンティクス — 関数内の変更が呼び出し側に見える
次の記事(#6 構造体とメソッド)ではGoのユーザー定義型 — structとそれに紐づくメソッド、そしてポインタレシーバまで整理します。