Day 6: 配列とスライス - 解説

学習の目的

  • 複数データの管理: 配列とスライスの使い分け
  • 動的なデータ構造: appendでの要素追加
  • 効率的な反復処理: for rangeの活用
  • メモリモデルの理解: スライスの内部構造を把握する
  • パフォーマンス最適化: 容量を意識したコーディング

---

概念の深掘り

1. 配列の基礎と特性

配列は固定長のデータ構造です。サイズは型の一部として扱われます。

var arr1 [5]int        // [0 0 0 0 0]
arr2 := [3]string{"a", "b", "c"}
arr3 := [...]int{1, 2, 3, 4}  // 要素数を自動で決定

重要な特性:

  • サイズは型の一部([5]int[10]intは異なる型)
  • 値渡しでコピーされる
  • サイズは実行時に変更不可
  • func modifyArray(arr [3]int) {
        arr[0] = 100  // 元の配列は変更されない(コピーが渡される)
    }
    
    func main() {
        a := [3]int{1, 2, 3}
        modifyArray(a)
        fmt.Println(a)  // [1 2 3] - 変更されていない
    }
    

    2. スライスの内部構造

    スライスは3つの要素で構成される参照型です:

  • ポインタ: 配列データへの参照
  • 長さ (len): 現在の要素数
  • 容量 (cap): 基底配列の要素数

slice := make([]int, 5, 10)
// ポインタ: 内部配列への参照
// 長さ: 5(現在の要素数)
// 容量: 10(再割り当てなしで格納可能な最大要素数)

視覚的イメージ:

スライス構造:
┌─────────┬─────┬─────┐
│ ポインタ │ len │ cap │
└────┬────┴─────┴─────┘
     │
     └──> [0][1][2][3][4][_][_][_][_][_]
          ←----- len=5 ----→
          ←--------- cap=10 ----------→

3. スライスの作成方法

方法1: リテラル

slice := []int{1, 2, 3, 4, 5}
// len=5, cap=5

方法2: make関数

// 長さと容量を指定
s1 := make([]int, 5, 10)  // len=5, cap=10

// 長さのみ指定(容量は長さと同じ)
s2 := make([]int, 5)      // len=5, cap=5

方法3: 配列からスライス

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4]  // [2 3 4], len=3, cap=4

4. スライスの操作とメモリ管理

append操作の仕組み

s := make([]int, 0, 3)
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))  // len=0 cap=3

s = append(s, 1)  // len=1 cap=3
s = append(s, 2)  // len=2 cap=3
s = append(s, 3)  // len=3 cap=3
s = append(s, 4)  // len=4 cap=6(容量が足りず再割り当て)

容量が不足した場合:

  • 新しい配列を割り当て(通常、容量を2倍に)
  • 既存要素をコピー
  • 新要素を追加
  • スライスのポインタを更新

// パフォーマンス最適化の例
// 悪い例:事前に容量を確保しない
badSlice := []int{}
for i := 0; i < 10000; i++ {
    badSlice = append(badSlice, i)  // 何度も再割り当てが発生
}

// 良い例:事前に容量を確保
goodSlice := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    goodSlice = append(goodSlice, i)  // 再割り当てなし
}

5. スライス演算の詳細

スライス式の構文

slice[low:high:max]
// low: 開始位置(含む)
// high: 終了位置(含まない)
// max: 容量の制限

例:

s := []int{0, 1, 2, 3, 4, 5}

s1 := s[1:4]      // [1 2 3], len=3, cap=5
s2 := s[1:4:4]    // [1 2 3], len=3, cap=3(容量を制限)

copy操作

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)

// srcからdstへコピー(コピーされる要素数は短い方)
n := copy(dst, src)
fmt.Println(dst)  // [1 2 3]
fmt.Println(n)    // 3(コピーされた要素数)

6. 配列 vs スライスの使い分け

特性 配列 スライス
サイズ 固定(コンパイル時) 動的(実行時)
メモリ 値型(コピーされる) 参照型(ポインタ共有)
サイズ変更 不可 可能(append)
使用頻度 非常に高い
適用場面 固定サイズ、メモリ効率重視 ほぼすべてのケース

配列を使うべきケース:

// 1. 数学的な固定サイズのデータ
type Matrix [3][3]float64

// 2. 低レベルプログラミング
var buffer [4096]byte

// 3. パフォーマンスクリティカルな場面
func hash(data [32]byte) [32]byte {
    // SHA-256ハッシュなど
}

スライスを使うべきケース(ほとんどの場合):

// データのサイズが可変
items := []string{}
items = append(items, "新しいアイテム")

// 関数に渡す場合(コピーを避ける)
func processItems(items []int) {
    // 効率的
}

---

アルゴリズムとパターン

1. スライスの反復処理パターン

パターン1: インデックスと値の両方が必要

numbers := []int{10, 20, 30, 40}
for i, value := range numbers {
    fmt.Printf("index: %d, value: %d\n", i, value)
}

パターン2: 値のみが必要

for _, value := range numbers {
    fmt.Println(value)
}

パターン3: インデックスのみが必要

for i := range numbers {
    fmt.Println(i)
}

パターン4: 従来のforループ

for i := 0; i < len(numbers); i++ {
    fmt.Println(numbers[i])
}

2. 一般的なスライス操作パターン

フィルタリング

func filter(numbers []int, predicate func(int) bool) []int {
    result := make([]int, 0, len(numbers))  // 容量を事前確保
    for _, num := range numbers {
        if predicate(num) {
            result = append(result, num)
        }
    }
    return result
}

// 使用例
evens := filter([]int{1, 2, 3, 4, 5}, func(n int) bool {
    return n%2 == 0
})
// evens = [2, 4]

マッピング

func mapInt(numbers []int, transform func(int) int) []int {
    result := make([]int, len(numbers))
    for i, num := range numbers {
        result[i] = transform(num)
    }
    return result
}

// 使用例
doubled := mapInt([]int{1, 2, 3}, func(n int) int {
    return n * 2
})
// doubled = [2, 4, 6]

リダクション(集約)

func reduce(numbers []int, initial int, reducer func(acc, val int) int) int {
    result := initial
    for _, num := range numbers {
        result = reducer(result, num)
    }
    return result
}

// 使用例:合計
sum := reduce([]int{1, 2, 3, 4}, 0, func(acc, val int) int {
    return acc + val
})
// sum = 10

3. スライスの結合と分割

結合

s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}

// 方法1: append
combined := append(s1, s2...)
// combined = [1, 2, 3, 4, 5, 6]

// 方法2: 事前に容量確保(効率的)
combined2 := make([]int, 0, len(s1)+len(s2))
combined2 = append(combined2, s1...)
combined2 = append(combined2, s2...)

要素の挿入

func insert(slice []int, index, value int) []int {
    // 容量を1増やす
    slice = append(slice, 0)
    // 挿入位置以降を右にシフト
    copy(slice[index+1:], slice[index:])
    // 値を挿入
    slice[index] = value
    return slice
}

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

要素の削除

func remove(slice []int, index int) []int {
    return append(slice[:index], slice[index+1:]...)
}

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

---

設計原則とベストプラクティス

1. nilスライスと空スライス

var nilSlice []int          // nil
emptySlice := []int{}       // 空だがnilではない
madeSlice := make([]int, 0) // 空だがnilではない

fmt.Println(nilSlice == nil)   // true
fmt.Println(emptySlice == nil) // false
fmt.Println(len(nilSlice))     // 0
fmt.Println(len(emptySlice))   // 0

// appendはどちらでも動作する
nilSlice = append(nilSlice, 1)    // OK
emptySlice = append(emptySlice, 1) // OK

推奨事項:

  • 関数の戻り値として空のスライスを返す場合はnilを返す
  • JSONエンコード時の違いに注意(nilnull、空スライスは[]

2. スライスの共有に注意

original := []int{1, 2, 3, 4, 5}
slice1 := original[0:3]  // [1, 2, 3]
slice2 := original[2:5]  // [3, 4, 5]

// slice1を変更するとoriginalとslice2にも影響
slice1[2] = 100
fmt.Println(original)  // [1, 2, 100, 4, 5]
fmt.Println(slice2)    // [100, 4, 5]

安全なコピー:

original := []int{1, 2, 3, 4, 5}
safeCopy := make([]int, len(original))
copy(safeCopy, original)
safeCopy[0] = 100
// originalは変更されない

3. パフォーマンス最適化

容量の事前確保

// 悪い例
var items []string
for i := 0; i < 1000; i++ {
    items = append(items, fmt.Sprintf("item%d", i))
    // 多数の再割り当てが発生
}

// 良い例
items := make([]string, 0, 1000)
for i := 0; i < 1000; i++ {
    items = append(items, fmt.Sprintf("item%d", i))
    // 再割り当てなし
}

大きなスライスの部分使用時の注意

func readFirstLine(filename string) []byte {
    data, _ := os.ReadFile(filename)  // 1GB
    firstLine := data[0:100]          // 100バイトだけ使う
    return firstLine
    // 問題: 1GB全体がメモリに残る
}

// 解決策: コピーを返す
func readFirstLine(filename string) []byte {
    data, _ := os.ReadFile(filename)
    firstLine := make([]byte, 100)
    copy(firstLine, data[0:100])
    return firstLine
    // dataは関数終了後にGCされる
}

---

メンタルモデル

スライスの理解を深めるイメージ

配列 = 本棚

  • 固定数の棚がある
  • 各棚には番号が付いている
  • 棚の数は変更できない

スライス = 動的な窓

  • 本棚(配列)を覗く窓
  • 窓の位置とサイズは変更可能
  • 複数の窓が同じ本棚を見ることができる
  • 配列: [0][1][2][3][4][5][6][7][8][9]
            ↑           ↑
    スライスA: [1][2][3][4][5]  (1:6)
    スライスB:     [2][3][4]    (2:5)
    

    appendのメンタルモデル

    初期状態:
    slice: [A][B][C][_][_]
           len=3, cap=5
    
    append(slice, D):
    slice: [A][B][C][D][_]
           len=4, cap=5 (容量に余裕あり)
    
    append(slice, E):
    slice: [A][B][C][D][E]
           len=5, cap=5 (容量いっぱい)
    
    append(slice, F):
    新配列確保: [A][B][C][D][E][F][_][_][_][_]
               len=6, cap=10 (容量2倍)
    

    ---

    よくある間違いと対策

    間違い1: rangeでのポインタ取得

    // 間違い
    var pointers []*int
    numbers := []int{1, 2, 3}
    for _, num := range numbers {
        pointers = append(pointers, &num)  // 全て同じアドレス
    }
    // 結果: 全てのポインタが最後の値(3)を指す
    
    // 正しい方法1: インデックスでアクセス
    for i := range numbers {
        pointers = append(pointers, &numbers[i])
    }
    
    // 正しい方法2: ローカル変数にコピー
    for _, num := range numbers {
        n := num
        pointers = append(pointers, &n)
    }
    

    間違い2: スライスのappend結果を無視

    // 間違い
    func addItem(slice []int, item int) {
        append(slice, item)  // 戻り値を無視
    }
    
    s := []int{1, 2}
    addItem(s, 3)
    fmt.Println(s)  // [1, 2] - 変更されていない
    
    // 正しい方法
    func addItem(slice []int, item int) []int {
        return append(slice, item)
    }
    
    s = addItem(s, 3)
    fmt.Println(s)  // [1, 2, 3]
    

    間違い3: スライスの比較

    // コンパイルエラー
    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    // if s1 == s2 { }  // エラー: スライスは比較できない
    
    // 正しい方法1: 手動で比較
    func equal(a, b []int) bool {
        if len(a) != len(b) {
            return false
        }
        for i := range a {
            if a[i] != b[i] {
                return false
            }
        }
        return true
    }
    
    // 正しい方法2: reflect.DeepEqualを使用
    import "reflect"
    if reflect.DeepEqual(s1, s2) {
        // 等しい
    }
    

    ---

    実践的な例

    例1: 重複削除

    func removeDuplicates(slice []int) []int {
        seen := make(map[int]bool)
        result := make([]int, 0, len(slice))
    
        for _, value := range slice {
            if !seen[value] {
                seen[value] = true
                result = append(result, value)
            }
        }
        return result
    }
    
    // 使用例
    input := []int{1, 2, 2, 3, 1, 4, 3}
    output := removeDuplicates(input)
    // output = [1, 2, 3, 4]
    

    例2: スライスの逆順

    func reverse(slice []int) {
        for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
            slice[i], slice[j] = slice[j], slice[i]
        }
    }
    
    // 使用例
    s := []int{1, 2, 3, 4, 5}
    reverse(s)
    // s = [5, 4, 3, 2, 1]
    

    例3: チャンク分割

    func chunk(slice []int, size int) [][]int {
        var chunks [][]int
        for i := 0; i < len(slice); i += size {
            end := i + size
            if end > len(slice) {
                end = len(slice)
            }
            chunks = append(chunks, slice[i:end])
        }
        return chunks
    }
    
    // 使用例
    numbers := []int{1, 2, 3, 4, 5, 6, 7}
    chunks := chunk(numbers, 3)
    // chunks = [[1, 2, 3], [4, 5, 6], [7]]
    

    ---

    セルフチェック問題

    基礎レベル

  • 次のコードの出力は何ですか?
s := make([]int, 3, 5)
fmt.Println(len(s), cap(s))

  • nilスライスと空スライスの違いは?
  • appendが新しい配列を割り当てるのはどんな時?
  • 中級レベル

  • このコードの問題点は?
func appendToSlice(s []int) {
    append(s, 100)
}

  • なぜこのコードは意図通りに動作しないのか?
s := []int{1, 2, 3}
for _, v := range s {
    v = v * 2
}
fmt.Println(s)  // [1, 2, 3] - なぜ?

上級レベル

  • このコードの出力を予測してください:
s := []int{1, 2, 3, 4, 5}
s1 := s[0:2]
s2 := s[1:3]
s1[1] = 100
fmt.Println(s, s1, s2)

  • パフォーマンスの観点から、どちらが優れているか説明してください:
// A
var result []int
for i := 0; i < 10000; i++ {
    result = append(result, i)
}

// B
result := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    result = append(result, i)
}

---

明日への準備

Day 7は「マップ(map)」を学びます。マップは配列やスライスと異なり、キーと値のペアを格納するデータ構造です。

// スライス: インデックス(整数)で値にアクセス
fruits := []string{"apple", "orange", "banana"}
fmt.Println(fruits[0])  // "apple"

// マップ: キー(任意の型)で値にアクセス
prices := map[string]int{
    "apple":  100,
    "orange": 80,
    "banana": 120,
}
fmt.Println(prices["apple"])  // 100

マップを使うことで:

  • データを意味のあるキーで管理できる
  • 高速な検索が可能(O(1))
  • 動的にキーを追加・削除できる
  • ---

    まとめ

    概念 説明 重要度
    配列 固定長のデータ列、値型 ★★☆
    スライス 可変長のデータ列、参照型 ★★★
    len() スライスの長さを取得 ★★★
    cap() スライスの容量を取得 ★★★
    append() スライスに要素を追加 ★★★
    copy() スライスをコピー ★★★
    range 反復処理のための構文 ★★★
    make() スライスの初期化 ★★★

    重要ポイント

  • スライスは参照型: 同じ基底配列を共有できる
  • appendの戻り値を使う: 必ずs = append(s, value)とする
  • 容量を意識する: パフォーマンス最適化のために事前確保
  • nilと空は違う: var s []int(nil)とs := []int{}(空)
  • rangeは値のコピー: ループ変数は元の要素のコピー
  • 次のステップ

  • Day 7でマップを学び、データ構造の理解を深める
  • スライスとマップを組み合わせた複雑なデータ構造を扱えるようになる
  • 実践的なアルゴリズム問題に挑戦する