こんにちは、かつコーチです。
「配列とスライス、結局どっちを使えばいいの?」。
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]
partにappendしただけのつもりが、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, 中級者向け, 基本文法