配列とスライス
配列とスライス
スーパーで牛乳のケースを買った場面を想像してください — 配列はその固定容量のケースで、一杯になるとこれ以上入らない;スライスは拡張し続けるショッピングバッグのようなものです — アイテムが増えたら、より大きなバッグに切り替えればいいのです。Goでは、配列は固定長で値型で、スライスは可変長で参照型です。実際にはスライスは配列よりはるかに頻繁に使用されますが、配列を理解することはスライスを理解するための基盤です。
1. コアコンセプト
| コンセプト | 説明 |
|---|---|
配列 [N]T |
固定長。宣言時にサイズを指定する必要がある。値型 — 代入とパラメータ渡しは配列全体をコピー |
スライス []T |
可変長。基本配列を参照。参照型 — 代入とパラメータ渡しは基本配列を共有 |
make([]T, len, cap) |
スライス作成の推奨方法。長さと容量を指定可能 |
append(slice, elems...) |
スライスに要素を追加し、新しいスライスを返す。基本配列の拡張が発生する可能性あり |
copy(dst, src) |
srcスライスからdstに内容をコピー。コピーした要素数を返す |
長さ len() |
スライス内の実際の要素数 |
容量 cap() |
スライスの開始位置から基本配列の末尾までの要素数 |
2. 基本構文/使い方
配列の宣言と初期化
// 長さ5のint配列を宣言。ゼロ値で初期化
var arr1 [5]int
// 宣言と初期化
var arr2 = [5]int{1, 2, 3, 4, 5}
// コンパイラに長さを推論させる
arr3 := [...]int{10, 20, 30}
// 特定のインデックスで初期化
arr4 := [5]int{1: 100, 3: 300} // [0, 100, 0, 300, 0]
スライスの宣言と初期化
// nilスライスを宣言(ゼロ値はnilだが、直接append可能)
var s1 []int
// リテラル初期化
s2 := []int{1, 2, 3}
// 配列からスライスを作成
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20, 30, 40]
// makeでスライスを作成
s4 := make([]int, 5) // 長さ5、容量5
s5 := make([]int, 3, 10) // 長さ3、容量10
nilスライスと空スライス([]int{}またはmake([]int, 0))は機能的に等価です — lenとcapはどちらも0で、appendは正常に動作します。違いは、nilスライスはJSONでnullにシリアライズされ、空スライスは[]にシリアライズされることです。
スライス操作
s := []int{10, 20, 30, 40, 50}
// スライスから部分スライス [左閉じ、右開き)
s1 := s[1:3] // [20, 30]
s2 := s[:3] // [10, 20, 30]、先頭から
s3 := s[2:] // [30, 40, 50]、末尾まで
s4 := s[:] // [10, 20, 30, 40, 50]、全体スライス
// 長さと容量
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 5
// 要素の追加
s = append(s, 60) // 単一要素を追加
s = append(s, 70, 80, 90) // 複数要素を追加
s = append(s, []int{100}...) // 別のスライスを追加
// スライスのコピー
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // dst = [1, 2, 3], n = 3
appendは新しいスライスを返すため、戻り値をキャプチャする必要があります。len(s) == cap(s)のとき、appendは新しい基本配列を割り当て、古いスライスと新しいスライスはデータを共有しなくなります。
s[low:high]でスライスする場合、新しいスライスは元のスライスと基本配列を共有します。部分スライスの要素を変更すると元にも影響します。独立したコピーが必要な場合はcopyを使用してください。
3. コード例
例1:基本的な使い方(難易度⭐)
package main
import "fmt"
func main() {
// ========== 配列 ==========
// 5つの点数の配列を宣言・初期化
scores := [5]int{90, 85, 78, 92, 88}
fmt.Println("点数配列:", scores)
// インデックスで要素にアクセス・変更
fmt.Println("最初の点数:", scores[0])
scores[2] = 80 // 3番目の点数を80に変更
fmt.Println("変更後:", scores)
// 配列の走査:従来のforループ
fmt.Println("\n--- 従来のfor走査 ---")
for i := 0; i < len(scores); i++ {
fmt.Printf("点数 %d: %d\n", i+1, scores[i])
}
// 配列の走査:range
fmt.Println("\n--- range走査 ---")
for index, value := range scores {
fmt.Printf("インデックス %d: %d\n", index, value)
}
// ========== スライス ==========
// 配列からスライスを派生
top3 := scores[0:3] // 最初の3つの点数
fmt.Println("\n最初の3つの点数:", top3)
// リテラルでスライスを作成
fruits := []string{"Apple", "Banana", "Orange"}
fmt.Println("フルーツスライス:", fruits)
// appendで要素を追加
fruits = append(fruits, "Grape")
fmt.Println("Grape追加後:", fruits)
// 長さと容量
fmt.Printf("長さ: %d、容量: %d\n", len(fruits), cap(fruits))
}
出力:
点数配列: [90 85 78 92 88]
最初の点数: 90
変更後: [90 85 80 92 88]
--- 従来のfor走査 ---
点数 1: 90
点数 2: 85
点数 3: 80
点数 4: 92
点数 5: 88
--- range走査 ---
インデックス 0: 90
インデックス 1: 85
インデックス 2: 80
インデックス 3: 92
インデックス 4: 88
最初の3つの点数: [90 85 80]
フルーツスライス: [Apple Banana Orange]
Grape追加後: [Apple Banana Orange Grape]
長さ: 4、容量: 4
例2:中級的な使い方(難易度⭐⭐)
package main
import "fmt"
func main() {
// ========== appendと拡張 ==========
// 容量3のスライスを作成
s := make([]int, 0, 3)
fmt.Printf("初期: len=%d, cap=%d, %v\n", len(s), cap(s), s)
// 一つずつ追加し、容量の変化を観察
for i := 1; i <= 5; i++ {
s = append(s, i)
fmt.Printf("%d追加後: len=%d, cap=%d, %v\n", i, len(s), cap(s), s)
}
// ========== 基本配列を共有するスライス ==========
original := []int{10, 20, 30, 40, 50}
sub := original[1:3] // [20, 30]
fmt.Println("\n--- 基本配列共有デモ ---")
fmt.Println("元のスライス:", original)
fmt.Println("部分スライス: ", sub)
// 部分スライスの変更は元のスライスに影響
sub[0] = 999
fmt.Println("\n部分スライス変更後:")
fmt.Println("元のスライス:", original) // original[1]も変更された
fmt.Println("部分スライス: ", sub)
// ========== copyで独立したコピーを作成 ==========
fmt.Println("\n--- copyで独立コピー ---")
original = []int{10, 20, 30, 40, 50}
// まずスライスし、その後独立コピー
subSlice := original[1:4] // [20, 30, 40]
independent := make([]int, len(subSlice))
copy(independent, subSlice)
independent[0] = 888
fmt.Println("元のスライス:", original) // 影響なし
fmt.Println("独立コピー: ", independent) // コピーのみ変更
// ========== 2つのスライスの結合 ==========
fmt.Println("\n--- スライス結合 ---")
a := []int{1, 2, 3}
b := []int{4, 5, 6}
merged := append(a, b...)
fmt.Println("結合結果:", merged)
}
出力:
初期: len=0, cap=3, []
1追加後: len=1, cap=3, [1]
2追加後: len=2, cap=3, [1 2]
3追加後: len=3, cap=3, [1 2 3]
4追加後: len=4, cap=6, [1 2 3 4]
5追加後: len=5, cap=6, [1 2 3 4 5]
--- 基本配列共有デモ ---
元のスライス: [10 20 30 40 50]
部分スライス: [20 30]
部分スライス変更後:
元のスライス: [10 999 30 40 50]
部分スライス: [999 30]
--- copyで独立コピー ---
元のスライス: [10 20 30 40 50]
独立コピー: [888 30 40]
--- スライス結合 ---
結合結果: [1 2 3 4 5 6]
例3:総合応用(難易度⭐⭐⭐)
package main
import "fmt"
// removeElementは指定されたインデックスの要素をスライスから削除します(順序維持)
func removeElement(s []int, index int) []int {
if index < 0 || index >= len(s) {
return s // インデックス範囲外、元のスライスを返す
}
// appendでインデックス前後の部分を結合
return append(s[:index], s[index+1:]...)
}
// insertElementは指定されたインデックスに要素を挿入します
func insertElement(s []int, index int, value int) []int {
if index < 0 || index > len(s) {
return s
}
// まずスライスを拡張し、要素をシフトし、代入
s = append(s, 0) // プレースホルダー要素を追加
copy(s[index+1:], s[index:]) // index以降の要素を1つずらす
s[index] = value // 対象位置に値を代入
return s
}
// filterEvenは偶数をフィルタリングし、新しいスライスを返します
func filterEven(s []int) []int {
result := make([]int, 0, len(s)/2) // 容量を半分と推定
for _, v := range s {
if v%2 == 0 {
result = append(result, v)
}
}
return result
}
// sliceInternalsはスライスのメモリ共有と拡張メカニズムをテストします
func sliceInternals() {
fmt.Println("=== スライス内部構造デモ ===")
// 基本配列を作成
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 部分スライスを切り出す(基本配列を共有)
s1 := data[2:5] // [2, 3, 4], len=3, cap=8
s2 := s1[1:3] // [3, 4], len=2, cap=7
fmt.Printf("data: %v, len=%d, cap=%d\n", data, len(data), cap(data))
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
// s2の要素変更はs1とdataに影響
s2[0] = 999
fmt.Printf("\ns2[0]=999変更後:\n")
fmt.Printf("data: %v\n", data) // data[4]が999に変更
fmt.Printf("s1: %v\n", s1) // s1[2]が999に変更
fmt.Printf("s2: %v\n", s2) // s2[0]が999に変更
// appendによる切断が発生する可能性
fmt.Println("\n--- appendによる拡張 ---")
s3 := data[0:2] // [0, 1], len=2, cap=10
fmt.Printf("append前 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
// 容量内でのappendはまだ共有
s3 = append(s3, 99)
fmt.Printf("append(99)後 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("data[2] = %d (変更された!)\n", data[2])
// 容量超過後、新しい配列が割り当てられる
s3 = append(s3, 100, 200, 300, 400, 500, 600, 700, 800)
fmt.Printf("大量append後 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("dataは影響なし: %v\n", data)
}
func main() {
// ========== 実践的なスライス操作 ==========
fmt.Println("=== 実践的なスライス操作 ===")
// 初期データ
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("元のデータ:", nums)
// インデックス4の要素を削除(値5)
nums = removeElement(nums, 4)
fmt.Println("インデックス4削除後:", nums)
// インデックス2に100を挿入
nums = insertElement(nums, 2, 100)
fmt.Println("インデックス2に100挿入後:", nums)
// 偶数をフィルタリング
evens := filterEven(nums)
fmt.Println("偶数スライス:", evens)
// ========== スライスをスタックとして使用 ==========
fmt.Println("\n=== スライスでスタックをシミュレート ===")
var stack []int
// プッシュ
for i := 1; i <= 5; i++ {
stack = append(stack, i)
fmt.Printf("プッシュ %d -> スタック: %v\n", i, stack)
}
// ポップ(末尾から)
for len(stack) > 0 {
// 末尾の要素を取得
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
fmt.Printf("ポップ %d -> スタック: %v\n", top, stack)
}
// ========== 内部構造デモ ==========
fmt.Println()
sliceInternals()
}
出力:
=== 実践的なスライス操作 ===
元のデータ: [1 2 3 4 5 6 7 8 9 10]
インデックス4削除後: [1 2 3 4 6 7 8 9 10]
インデックス2に100挿入後: [1 2 100 3 4 6 7 8 9 10]
偶数スライス: [2 100 4 6 8 10]
=== スライスでスタックをシミュレート ===
プッシュ 1 -> スタック: [1]
プッシュ 2 -> スタック: [1 2]
プッシュ 3 -> スタック: [1 2 3]
プッシュ 4 -> スタック: [1 2 3 4]
プッシュ 5 -> スタック: [1 2 3 4 5]
ポップ 5 -> スタック: [1 2 3 4]
ポップ 4 -> スタック: [1 2 3]
ポップ 3 -> スタック: [1 2]
ポップ 2 -> スタック: [1]
ポップ 1 -> スタック: []
=== スライス内部構造デモ ===
data: [0 1 2 3 4 5 6 7 8 9], len=10, cap=10
s1: [2 3 4], len=3, cap=8
s2: [3 4], len=2, cap=7
s2[0]=999変更後:
data: [0 1 2 3 999 5 6 7 8 9]
s1: [2 3 999]
s2: [3 999]
--- appendによる拡張 ---
append前 s3: [0 1], len=2, cap=10
append(99)後 s3: [0 1 99], len=3, cap=10
data[2] = 99 (変更された!)
大量append後 s3: [0 1 99 100 200 300 400 500 600 700 800], len=12, cap=20
dataは影響なし: [0 1 99 3 999 5 6 7 8 9]
3. よくあるユースケース
ケース1:バッチデータ処理(フィルタリングと変換)
package main
import "fmt"
// cleanDataは文字列スライスから空文字列をフィルタリングし、大文字に変換します
func cleanData(input []string) []string {
result := make([]string, 0, len(input))
for _, s := range input {
if s != "" {
// 大文字に変換(簡略化例、実際にはstrings.ToUpperを使用)
upper := ""
for _, c := range s {
if c >= 'a' && c <= 'z' {
upper += string(c - 32)
} else {
upper += string(c)
}
}
result = append(result, upper)
}
}
return result
}
func main() {
raw := []string{"hello", "", "world", "", "go", "lang"}
cleaned := cleanData(raw)
fmt.Println("クリーニング後:", cleaned) // [HELLO WORLD GO LANG]
}
ケース2:動的キューの実装
package main
import "fmt"
// Queueはスライスを使ったシンプルなFIFOキューを実装します
type Queue struct {
items []string
}
// Enqueueはキューにアイテムを追加します
func (q *Queue) Enqueue(item string) {
q.items = append(q.items, item)
}
// Dequeueは最初のアイテムを削除して返します
func (q *Queue) Dequeue() (string, bool) {
if len(q.items) == 0 {
return "", false
}
item := q.items[0]
q.items = q.items[1:] // 最初の要素を削除
return item, true
}
// Sizeはキューのサイズを返します
func (q *Queue) Size() int {
return len(q.items)
}
func main() {
q := &Queue{}
q.Enqueue("タスク A")
q.Enqueue("タスク B")
q.Enqueue("タスク C")
fmt.Printf("キューサイズ: %d\n", q.Size())
for q.Size() > 0 {
item, _ := q.Dequeue()
fmt.Println("処理中:", item)
}
}
❓ よくある質問
質問1:var s []intで宣言したスライスはnilスライスですか?直接使えますか?
はい。nilスライスのlenとcapは0で、appendは完全に動作します。Go公式の推奨:スライスがnilの可能性がある場合、使用前に余分なnilチェックは不要で、直接appendしてください。
var s []int // nilスライス
s = append(s, 1, 2) // 完全に有効
fmt.Println(s) // [1 2]
質問2:s[2:5]でスライスすると元のスライスに影響しますか?
はい。スライスで作成された部分スライスは元のスライスと基本配列を共有します。部分スライスの要素を変更すると元にも反映されます。独立したコピーにはcopyを使用してください:
original := []int{1, 2, 3, 4, 5}
sub := make([]int, 3)
copy(sub, original[2:5]) // 独立コピー、相互影響なし
質問3:なぜappend後に値が変わったのですか?
len(s) < cap(s)のとき、appendは新しい配列を作成せずに基本配列に直接書き込みます。他のスライスが同じ基本配列の後方の位置を参照している場合、「予期しない」変更が見えることがあります。解決策:copyで独立したコピーを作成してからappendしてください。
質問4:スライスの容量はどのように成長しますか?
Goランタイムは現在のスライス容量に基づいて新しい容量を決定します。典型的な戦略:1024未満のときは容量を2倍に、1024以上のときは約1.25倍に成長させます。具体的な戦略はGoバージョン間で変わる可能性があるため、正確な成長ルールには依存しないでください。
📖 まとめ
- 配列
[N]Tは固定長で値型。代入/パラメータ渡しは配列全体をコピー - スライス
[]Tは可変長で参照型。基本配列を参照 make([]T, len, cap)はスライス作成の推奨方法。容量を事前割り当てすると頻繁な拡張を回避appendは新しいスライスを返すため、戻り値をキャプチャする必要がある。容量不足時は新しい基本配列を割り当て- スライスのスライシング
s[low:high]は元のスライスと基本配列を共有し、変更は相互に影響 copy(dst, src)はスライスの独立したコピーを作成len()は要素数を返し、cap()は基本配列の容量を返す- スライスのゼロ値は
nilで、直接append可能
📝 演習
演習1(⭐)
10個の整数(値1〜10)のスライスを作成し、以下を実行するプログラムを書いてください:
- スライスの長さと容量を出力
- インデックス2から7までをスライスし、その内容、長さ、容量を出力
- rangeで部分スライスをイテレーションし、各要素とそのインデックスを出力
演習2(⭐⭐)
[]intスライスを受け取り、重複を除去した新しいスライスを返すunique関数を書いてください(最初に出現する順序を維持)。例えば、入力[1, 3, 2, 3, 1, 4, 2]は[1, 3, 2, 4]を返すべきです。
ヒント:既に出現した要素を記録する補助スライスを使用してください。
演習3(⭐⭐⭐)
2つのソート済み[]intスライスを受け取り、1つのソート済みスライスにマージするmergeSorted関数を書いてください。要件:
- 時間計算量はO(n)でなければならない。マージ後にソートしてはいけない
- 例えば、入力
[1, 3, 5, 7]と[2, 4, 6, 8]は[1, 2, 3, 4, 5, 6, 7, 8]を返すべきです - 考察:この操作はマージソートでどのような役割を果たしますか?



