Go言語の配列・スライス・マップ
2026/09/13

配列

配列[n]Tは、型Tの要素を固定数n個並べたデータ構造です。要素数nは型情報の一部であり、[3]intと[4]intは異なる型として扱われます。型が異なるため、相互に代入することはできません。

var a [3]int
// var b [4]int = a // コンパイルエラー: 型が異なる

Go言語の配列は値型です。配列を変数に代入したり関数へ引数として渡したりすると、すべての要素が丸ごとコピーされます。

arr := [3]int{1, 2, 3}
dup := arr                  // 配列全体のコピー
dup[0] = 0
fmt.Println(arr[0], dup[0]) // 1, 0(元の配列arrは変わらない)

配列の要素数が多い場合、丸ごとのコピーはオーバーヘッドになります。また、長さが型に固定されているため、要素数が可変のデータを扱う用途には適していません。そのため、Go言語では可変長データを扱うコレクションとして主にスライス[]Tを用います。

スライス

スライス([]T)は、配列の連続した領域を参照する可変長のデータ構造です。

スライス値の実体は、元配列へのポインタ、参照している要素数(len)、参照可能な最大容量(cap)の3つのフィールドからなるヘッダです。capは再割り当てなしに要素を追加できる上限を表します。

スライスを変数へ代入したり関数へ渡したりした際にコピーされるのは、この小さなヘッダのみです。元配列の実体は共有されるため、一方のスライスを経由して要素を書き換えると、同じ元配列を参照している他のスライスにも変更が反映されます。

スライスはリテラルや組み込み関数makeで作成できます。

nums := []int{1, 2, 3}
buf := make([]byte, 0, 64) // len=0, cap=64

s[low:high]というスライス式は、元配列の一部を参照する新しいスライスを生成します。スライス自体は別個に作られますが、要素が格納されている元配列は共有されます。例えばall[low:high]と書くと、インデックスlow以上high未満の要素を切り出します。

all := []int{0, 1, 2, 3, 4}
mid := all[1:4]  // midは[1, 2, 3]、len=3

mid[0] = 99
fmt.Println(all) // [0 99 2 3 4](元配列が書き換わるためall側にも反映される)

s[low:high]で生成されたスライスの容量はcap(元) - lowです。元のallのcapが5、lowが1の場合、切り出されたmidのcapは4になります。

fmt.Println(len(mid), cap(mid)) // 3, 4

s[low:high:max]というスライス式を使うと、スライスの容量をmax - lowに明示的に制限できます。部分スライスが元配列の空き容量を意図せず上書きするのを防ぎたい場合に有効です。

fullCap := all[1:4:4]                   // len=3, cap=3(max - low = 4 - 1)
fmt.Println(len(fullCap), cap(fullCap)) // 3, 3

スライスに対してappendを実行した際の挙動は、そのスライスのcapに空きがあるかによって異なります。capに空きがある場合、新しい配列は確保されず、共有している元配列へ直接値が書き込まれます。元配列の要素に既に値がある場合は、その要素の値が上書きされます。一方、capに空きがない場合、別のメモリ領域に新しい配列を丸ごと確保し、既存の要素をコピーした上で末尾に追記します。部分スライスの参照先はこの新しい配列を指すように切り替わるため、元のスライスや元配列の内容は書き換えられず、参照関係が切り離されます。

all := []int{0, 1, 2, 3, 4}
wide := all[1:4]            // len=3, cap=4(元配列のインデックス4に既に値がある)
wide = append(wide, 9)
fmt.Println(all)            // [0 1 2 3 9](元配列のall[4]が9で上書きされる)

all = []int{0, 1, 2, 3, 4}
tight := all[1:4:4]         // len=3, cap=3(空き容量なし)
tight = append(tight, 9)    // 新しい配列が別個に確保されてコピーされ、tightの参照先が切り替わる
fmt.Println(all)            // [0 1 2 3 4](元のallや元配列は影響を受けない)

スライス間の要素共有を切り離したい場合は、makeで独立した元配列を確保し、組み込み関数copyで要素を複製します。

all := []int{0, 1, 2, 3, 4}
mid := all[1:4]              // [1, 2, 3]
dst := make([]int, len(mid)) // 独立した新しい元配列を確保
copy(dst, mid)               // 要素をコピー

dst[0] = 0
fmt.Println(dst[0], mid[0])  // 0, 1(dstの変更はmidやallに影響しない)

未初期化のnilスライスであっても、lenやcapの取得、appendによる要素追加は正常に動作します。

var s []int
fmt.Println(s == nil, len(s), cap(s)) // true 0 0

s = append(s, 10, 20)
fmt.Println(s, s == nil)              // [10 20] false

appendは要素を追加した新しいスライスを返すため、戻り値を変数へ代入し直さないと長さ(len)の変更が反映されません。

nums := []int{1, 2, 3}
nums = append(nums, 4, 5)   // [1, 2, 3, 4, 5]

s := []int{1}
append(s, 2)                // 戻り値を捨てるとsのlenは1のまま
s = append(s, 2)            // 再代入することでlen=2になる

v := []int{1}
t := v
t = append(t, 99)           // 変数tのみlen=2に更新
fmt.Println(len(v), len(t)) // 1, 2(vは変化しない)

関数へ引数としてスライスを渡す場合も、値渡しによりスライスの情報がコピーされます。関数内でappendを実行しても、呼び出し元のスライス変数のlenは変化しません。ただし、capに空きがあって元配列の再割り当てが発生しなかった場合は、元配列の内容自体が関数内から書き換えられることがあります。

func grow(s []int) {
    s = append(s, 99)            // 関数内の変数sのみlen=2になる
}

data := make([]int, 1, 2)        // len=1, cap=2
grow(data)
fmt.Println(len(data))           // 1(呼び出し元のスライスのlenは不変)
fmt.Println(data[1])             // 99(capに空きがあったため元配列には書き込まれている)

narrow := []int{1}               // len=1, cap=1
grow(narrow)
fmt.Println(len(narrow), narrow) // 1, [1](cap不足で別配列に再割り当てされたため影響なし)

スライスを入れ子にすることで、多次元データ構造を表現できます。

rows, cols := 2, 3
matrix := make([][]int, rows)
for i := range matrix {
    matrix[i] = make([]int, cols)
}

スライスをfor-rangeで走査する際、ループ内で要素を追加してスライス自体の長さを変更すると、意図しない無限ループや要素の読み飛ばしにつながる恐れがあります。走査対象のスライス自体をループ内で変更することは避けてください。

// 避けるべき例: 走査中にスライスを変更する
s := []int{1, 2, 3}
for i, v := range s {
    s = append(s, v) // 反復対象や終了条件が不定になる
}

マップ

マップ(map[K]V)は、キーと値を結びつけるハッシュテーブルです。

キー型Kには、比較演算子==および!=が定義されている型(comparableな型)のみ指定できます。スライス、マップ、関数型は比較できないためキーに指定できません。構造体は、すべてのフィールドの型が比較可能である場合に限りキーとして使用できます。

makeまたはリテラルを用いて初期化し、キーを指定して要素の読み書きを行います。

scores := make(map[string]int) // map[string]int{"go": 90} でも初期化可能
scores["go"] = 90
scores["rust"] = 85

キーの存在確認には2値受け取りを用います。指定したキーが存在しない場合、2番目の戻り値okがfalseとなり、1番目の戻り値には値型のゼロ値が返ります。

fmt.Println(scores["go"]) // 90

v, ok := scores["java"]
fmt.Println(v, ok)        // 0, false(キーが存在しない)

不要になったキーは組み込み関数deleteで削除します。

delete(scores, "go")
fmt.Println(scores["go"], scores["rust"]) // 0, 85

未初期化のマップ変数はnilです。nilマップからの読み取りやdeleteの実行はパニックになりませんが、要素を書き込もうとすると実行時パニックが発生します。

var m map[string]int
// m["k"] = 1    // panic: nilマップへの書き込み

v, ok := m["k"] // v=0, ok=false(読み取りは安全)
delete(m, "k")  // キーが存在しなくても安全

マップをfor-rangeで走査する際の順序は、言語仕様として保証されていません。実装上も走査ごとに順序がランダム化され、プログラムが特定の反復順序に依存しないよう設計されています。

scores := map[string]int{"go": 90, "rust": 85, "java": 80}
for lang, score := range scores {
    fmt.Println(lang, score)
}
// 走査を実行するたびに出力順が変わることがある

キーの昇順など一定の順序で処理したい場合は、一度キーをスライスに集めてソートしてから参照します。

keys := make([]string, 0, len(scores))
for lang := range scores {
    keys = append(keys, lang)
}
sort.Strings(keys)

for _, lang := range keys {
    fmt.Println(lang, scores[lang]) // go, java, rustの昇順で出力
}