【Go】配列とスライスの違い・使い方

Go

こんにちは、かつコーチです。

「配列とスライス、結局どっちを使えばいいの?」。
Goを学び始めると、必ずと言っていいほどぶつかる疑問です。
結論から言うと、実務ではスライスを使う場面がほとんどです。
この記事では、配列スライスの違いを整理しつつ、appendの挙動や内部構造も中級者向けに解説します。

配列とスライスとは?

配列:長さが固定のデータの入れ物

配列とは、あらかじめ決めた個数の値をまとめて扱うデータ構造です。
Goの配列は、宣言した時点で長さが確定し、あとから変更できません。

package main

import "fmt"

func main() {
	var nums [3]int
	nums[0] = 10
	nums[1] = 20
	nums[2] = 30
	fmt.Println(nums)
}

[3]intという書き方が「int型の値を3つ入れる配列」という意味です。
長さも型の一部として扱われるため、[3]int[4]intはまったく別の型として扱われます。

スライス:長さを変えられる可変長のデータ構造

スライスとは、配列を土台にしながら、要素数を自由に増減できるデータ構造です。
実務のGoコードでは、配列よりもスライスを使う場面が圧倒的に多くなります。

package main

import "fmt"

func main() {
	nums := []int{10, 20, 30}
	nums = append(nums, 40)
	fmt.Println(nums)
}

[3]intのように長さを書かず[]intとだけ書くと、それはスライスの宣言になります。
append(スライスの末尾に要素を追加する組み込み関数)で、要素数を後から増やせるのが最大の特徴です。

なぜスライスが実務の主役になるのか

実際のアプリケーションでは、データの件数が事前に分からないケースがほとんどです。
データベースの検索結果やAPIのレスポンスなど、件数が可変な場面でスライスは欠かせません。
配列は「サイズが完全に固定されている」ことが分かっている、ごく限られた場面でのみ使われます。

スライスの基本操作

手順1:スライスの宣言と初期化

スライスは、いくつかの方法で作成できます。

package main

import "fmt"

func main() {
	var s1 []int          // nilスライス(中身が空)
	s2 := []int{1, 2, 3}  // 値を指定して初期化
	s3 := make([]int, 3)  // 長さ3のスライスを作成
	fmt.Println(s1, s2, s3)
}

make(スライスやマップなどを初期化するための組み込み関数)を使うと、長さと容量を指定して作成できます。

手順2:appendで要素を追加する

appendはスライス操作の中心となる関数です。

package main

import "fmt"

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

appendは必ず戻り値を元の変数に代入し直す必要があります。
これを忘れると、追加した結果が反映されないバグになるので注意してください。

スライスの内部構造:ポインタ・長さ・容量

スライスは、内部的に3つの情報を持つ小さな構造体です。

  • ポインタ:実際のデータが入っている配列の先頭住所(値そのものではなく、値がしまってある住所を指し示す仕組み)
  • 長さ(len):現在使っている要素数
  • 容量(cap):追加のメモリ確保なしで格納できる最大要素数
package main

import "fmt"

func main() {
	s := make([]int, 2, 5)
	fmt.Println(len(s), cap(s)) // 2 5
}

容量を超えてappendすると、Goはより大きな配列を新しく確保し、中身をコピーします。
この仕組みを知らないと、次に説明するような予想外の挙動に出会うことになります。

よくあるつまずきポイント・エラー対処

スライスの共有による予期しない書き換え

私が実務でGoを書いていたとき、あるスライスから別のスライスを作って一部だけ使うつもりが、思わぬバグに繋がったことがあります。

❌Before:元のスライスを書き換えてしまうコード

package main

import "fmt"

func main() {
	original := []int{1, 2, 3, 4, 5}
	part := original[0:2]
	part = append(part, 99)
	fmt.Println("original:", original)
	fmt.Println("part:", part)
}

実行すると、次のような結果になりました。

original: [1 2 99 4 5]
part: [1 2 99]

partappendしただけのつもりが、originalの3番目の要素まで書き換わってしまいました。
原因はoriginal[0:2]で作ったpartにあります。
originalと同じ配列を指すポインタを共有していたことが原因でした。
partの容量には余裕があったため、新しい配列を確保せず、共有元の配列にそのまま書き込んでしまったのです。
このときは半日近く原因が分からず、fmt.Printlnを大量に埋め込んでようやく気づきました。

✅After:copyまたはフルスライス式で意図的に切り離す

package main

import "fmt"

func main() {
	original := []int{1, 2, 3, 4, 5}
	part := make([]int, 2)
	copy(part, original[0:2])
	part = append(part, 99)
	fmt.Println("original:", original)
	fmt.Println("part:", part)
}

copy(スライスの中身を別のスライスにコピーする組み込み関数)で明示的に複製すれば、元のスライスに影響しません。
スライスを切り出して使うときは「配列を共有しているかもしれない」と常に意識することが大切です。

応用・一歩先の使い方

大量データを扱う場面では、appendのたびに配列の再確保が起きないよう、あらかじめ容量を確保しておくのが定石です。

package main

import "fmt"

func main() {
	s := make([]int, 0, 100) // 長さ0、容量100で確保
	for i := 0; i < 100; i++ {
		s = append(s, i)
	}
	fmt.Println(len(s), cap(s))
}

最初からcapを十分に確保しておくことで、内部の配列コピーが発生せず、パフォーマンスが安定します。
中級者になったら、処理する件数の見積もりに応じてmakeの第3引数を調整する習慣をつけましょう。

まとめ

この記事のポイント

  • 配列は長さ固定、スライスは長さ可変で、実務ではスライスが主役
  • スライスは内部にポインタ・長さ・容量の3つの情報を持つ
  • appendは戻り値を変数に代入し直す必要がある
  • スライス同士が配列を共有していると、片方の変更がもう片方に影響することがある
  • 意図的に切り離したいときはcopyを使う

次に読むべき記事

スライスの次は、キーと値のペアを扱う「マップ(map)の基本:キーと値の扱い方」に進みましょう。
Goのデータ構造の基本がひと通りつながります。

タグ: Go, 中級者向け, 基本文法

タイトルとURLをコピーしました