[Goのはじめ方] Part8: 配列とスライス

Go

はじめに

Go言語で複数のデータをまとめて扱う基本的な方法として、「配列」と「スライス」があります。これらは似ているようで、実は重要な違いがあります 🤔。 このステップでは、配列とスライスのそれぞれの特徴と使い方、そしてその違いについて学んでいきましょう。 データを効率的に扱うための重要な概念なので、しっかりマスターしましょう!💪

配列 (Array) 🔢

配列は、同じ型の要素を固定された数だけ格納できるデータ構造です。一度作成すると、そのサイズ(要素数)を変更することはできません。

定義と初期化

配列は [要素数]型 という形式で定義します。

package main

import "fmt"

func main() {
    // 要素数3のint型配列を宣言 (各要素はゼロ値の0で初期化)
    var arr1 [3]int
    fmt.Println("arr1:", arr1) // 出力: arr1: [0 0 0]

    // 宣言と同時に初期化 (リテラル)
    arr2 := [3]int{10, 20, 30}
    fmt.Println("arr2:", arr2) // 出力: arr2: [10 20 30]

    // 要素数から[...]で推論して初期化
    arr3 := [...]string{"Apple", "Banana", "Cherry"}
    fmt.Println("arr3:", arr3) // 出力: arr3: [Apple Banana Cherry]
    fmt.Println("arr3の要素数:", len(arr3)) // 出力: arr3の要素数: 3
}

要素へのアクセス

配列の要素には、インデックス(0から始まる番号)を使ってアクセスします。

package main

import "fmt"

func main() {
    arr := [3]int{10, 20, 30}

    // 最初の要素 (インデックス0) を取得
    fmt.Println("最初の要素:", arr[0]) // 出力: 最初の要素: 10

    // 2番目の要素 (インデックス1) を変更
    arr[1] = 25
    fmt.Println("変更後の配列:", arr) // 出力: 変更後の配列: [10 25 30]
}

配列の注意点 ⚠️

  • 固定長: 一度定義した配列のサイズは変更できません。要素を追加したり削除したりすることはできません。
  • 値渡し: 関数に配列を渡すと、配列全体のコピーが渡されます。関数内で配列の要素を変更しても、元の配列には影響しません。
  • 型: 配列の要素数も型の一部です。[3]int[4]int は異なる型として扱われます。
package main

import "fmt"

func modifyArray(a [3]int) {
    a[0] = 100 // 関数内で配列を変更
    fmt.Println("関数内:", a)
}

func main() {
    arr := [3]int{10, 20, 30}
    fmt.Println("変更前:", arr)
    modifyArray(arr) // 配列を関数に渡す (コピーが渡される)
    fmt.Println("変更後:", arr) // 元の配列は変わらない
}
// 出力:
// 変更前: [10 20 30]
// 関数内: [100 20 30]
// 変更後: [10 20 30]

配列は固定長であるため、要素数がプログラム実行前に決まっている場合などに使われますが、Goでは後述するスライスの方がより一般的に使われます。

スライス (Slice) 🍕

スライスは、可変長のシーケンス(連続したデータの並び)を扱うための、より柔軟で強力なデータ構造です。配列と似ていますが、サイズを動的に変更できます。 内部的には配列を参照しており、「配列の一部を切り出したビュー」と考えることができます。

定義と初期化

スライスは []型 という形式で定義します。配列と異なり、[]の中に要素数を指定しません。

package main

import "fmt"

func main() {
    // スライスリテラルで初期化
    s1 := []int{10, 20, 30}
    fmt.Println("s1:", s1) // 出力: s1: [10 20 30]

    // make関数で作成 (型, 長さ, 容量(省略可))
    // 長さ3, 容量3のスライスを作成 (ゼロ値で初期化)
    s2 := make([]string, 3)
    fmt.Println("s2:", s2) // 出力: s2: [  ] (空文字列3つ)
    fmt.Printf("s2: len=%d, cap=%d\n", len(s2), cap(s2)) // 出力: s2: len=3, cap=3

    // 長さ2, 容量5のスライスを作成
    s3 := make([]float64, 2, 5)
    fmt.Println("s3:", s3) // 出力: s3: [0 0]
    fmt.Printf("s3: len=%d, cap=%d\n", len(s3), cap(s3)) // 出力: s3: len=2, cap=5

    // 配列からスライスを作成 ([開始インデックス:終了インデックス(含まない)])
    arr := [5]int{1, 2, 3, 4, 5}
    s4 := arr[1:4] // インデックス1から3 (4の手前) まで
    fmt.Println("s4:", s4) // 出力: s4: [2 3 4]
    fmt.Printf("s4: len=%d, cap=%d\n", len(s4), cap(s4)) // 出力: s4: len=3, cap=4 (元の配列の最後までが容量)

    // nilスライス (初期化されていないスライス)
    var s5 []int
    fmt.Println("s5:", s5, s5 == nil) // 出力: s5: [] true
    fmt.Printf("s5: len=%d, cap=%d\n", len(s5), cap(s5)) // 出力: s5: len=0, cap=0
}

要素へのアクセスと変更

配列と同様に、インデックスを使って要素にアクセス・変更できます。

package main

import "fmt"

func main() {
    s := []string{"Go", "Java", "Python"}
    fmt.Println("元のスライス:", s) // 出力: 元のスライス: [Go Java Python]

    fmt.Println("2番目の要素:", s[1]) // 出力: 2番目の要素: Java

    s[1] = "Rust" // 要素を変更
    fmt.Println("変更後のスライス:", s) // 出力: 変更後のスライス: [Go Rust Python]
}

スライスの特徴と内部構造

  • 可変長: append 関数を使って要素を追加できます。
  • 参照型: スライスは内部の配列への参照を持っています。関数にスライスを渡すと、この参照情報がコピーされます。そのため、関数内でスライスの要素を変更すると、元のスライスにも影響が及びます(配列の値渡しとは異なります)。
  • 内部構造: スライスは内部的に3つの情報を持っています。
    • 参照先の配列へのポインタ (Pointer)
    • スライスに含まれる要素の数を示す長さ (Length)
    • 参照先の配列の先頭から、スライスが拡張可能な最大要素数を示す容量 (Capacity)
package main

import "fmt"

func modifySlice(sl []string) {
    sl[0] = "Modified" // スライスの要素を変更
    fmt.Println("関数内:", sl)
}

func main() {
    s := []string{"Original", "Value"}
    fmt.Println("変更前:", s)
    modifySlice(s) // スライスを関数に渡す (参照情報がコピーされる)
    fmt.Println("変更後:", s) // 元のスライスの要素も変更される
}
// 出力:
// 変更前: [Original Value]
// 関数内: [Modified Value]
// 変更後: [Modified Value]

len() と cap() 関数

組み込み関数の len()cap() を使うと、スライスの長さと容量を取得できます。

  • len(s): スライス s の現在の要素数を返します。
  • cap(s): スライス s の基になる配列の先頭要素から数えて、スライスが利用可能な要素数を返します。
package main

import "fmt"

func main() {
    s := make([]int, 3, 5) // 長さ3, 容量5のスライス
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // 出力: len=3 cap=5 [0 0 0]

    s = append(s, 1)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // 出力: len=4 cap=5 [0 0 0 1]

    s = append(s, 2)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // 出力: len=5 cap=5 [0 0 0 1 2]

    // 容量(5)を超える要素を追加すると、新しい配列が確保され、容量が増える
    // 容量の増加量はGoのバージョンや要素数によって変動する可能性がありますが、一般的には元の2倍程度になることが多いです。
    s = append(s, 3)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // 出力例: len=6 cap=10 [0 0 0 1 2 3]
}

スライスの操作: append と copy

  • append(slice, 要素...): スライスの末尾に1つ以上の要素を追加します。元のスライスの容量が足りない場合は、より大きな容量を持つ新しい配列が内部的に確保され、そこへの参照を持つ新しいスライスが返されます。append の結果は必ず元のスライス変数に再代入する必要があります。
  • copy(dst, src): スライス src からスライス dst へ要素をコピーします。コピーされる要素数は、len(dst)len(src) のうち小さい方になります。copy はコピーした要素数を返します。
package main

import "fmt"

func main() {
    // append の例
    s1 := []int{1, 2}
    s1 = append(s1, 3)        // 1要素追加
    fmt.Println("s1:", s1)     // 出力: s1: [1 2 3]
    s1 = append(s1, 4, 5, 6) // 複数要素追加
    fmt.Println("s1:", s1)     // 出力: s1: [1 2 3 4 5 6]

    s2 := []int{7, 8}
    s1 = append(s1, s2...)   // スライスを結合 (末尾に...をつける)
    fmt.Println("s1:", s1)     // 出力: s1: [1 2 3 4 5 6 7 8]

    // copy の例
    src := []string{"A", "B", "C"}
    dst := make([]string, 2) // 長さ2のコピー先スライス

    numCopied := copy(dst, src)
    fmt.Println("dst:", dst)           // 出力: dst: [A B] (srcの最初の2要素がコピーされる)
    fmt.Println("コピーされた要素数:", numCopied) // 出力: コピーされた要素数: 2
}

配列とスライスの違い まとめ 📊

これまで見てきた配列とスライスの主な違いを表にまとめます。

特徴 配列 (Array) スライス (Slice)
サイズ 固定長 (定義時に決定、変更不可) 可変長 (append で変更可能)
定義方法 [要素数]型 (例: [3]int) []型 (例: []int), make([]型, 長さ, 容量)
内部構造 連続したメモリ領域に値そのもの 内部配列へのポインタ、長さ (len)、容量 (cap)
関数への引渡し 値渡し (全体のコピーが渡される) 参照渡しのような振る舞い (参照情報がコピーされる)
ゼロ値 要素がゼロ値で初期化された配列 nil (長さ0, 容量0)
柔軟性 低い 高い ✨
主な用途 要素数が完全に固定の場合 (まれ) ほとんどの場合 (コレクション操作全般)

使い分けの指針

  • 要素数が固定で、実行中に変わることが絶対にない場合は、配列を使うことも考えられます。(例:RGB値を格納するなど)
  • 要素数が可変である、または要素の追加・削除が必要な場合は、スライスを使いましょう。Goのプログラムでは、ほとんどの場合スライスが使われます。
  • 関数の引数や戻り値として複数の要素を扱う場合も、通常はスライスを使用します。

基本的には、迷ったらスライスを使うと考えておけば問題ないでしょう!🚀

まとめ

このステップでは、Go言語における配列とスライスの違いと基本的な使い方を学びました。

  • 配列は固定長のデータコンテナ。
  • スライスは可変長で柔軟なデータコンテナであり、内部的に配列を参照している。
  • スライスには長さ (length)容量 (capacity) があり、appendcopy などの便利な組み込み関数が用意されている。
  • Goでは、一般的にスライスの方がよく利用される。

配列とスライスの特性を理解することで、より効率的で柔軟なGoプログラムを書くことができます。次のステップでは、もう一つの重要なデータ構造である「マップ」について学びます!🎉

コメント

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