Go 複合型:配列とスライス
配列 (Arrays) と スライス (Slices) は、Go言語において最も基礎的な複合データ型であり、複数の同じ型の値を一つの変数名の下にまとめることができます。
簡単に言えば、配列は要素を格納するための「固定サイズかつ連続したメモリ空間」を提供し、スライスはその背後にある配列(基底配列)にアクセスして操作するための、より柔軟で動的な「ビュー(視点)」を提供します。
本章では、配列とスライスの内部ロジックを深く掘り下げ、宣言、初期化、操作のテクニック、およびGo言語特有の注意点について詳しく解説します。
1. Go言語における配列 (Arrays)
配列は、固定された長さを持つ同じ型の要素のシーケンスです。Go言語において、配列の長さはその「型」の一部として扱われます。つまり、[3]int と [4]int は全く別の型であり、直接代入したり混用したりすることはできません。
1.1 配列の宣言
配列を宣言する基本構文は以下の通りです:
var 配列名 [配列の長さ]データ型例えば、5つの整数を格納できる配列を宣言する場合:
var numbers [5]intこのコードは numbers という名前の配列を宣言します。要素は自動的にその型の ゼロ値(整数の場合は 0)で初期化されます。
1.2 配列の初期化
宣言と同時に配列を初期化する方法はいくつかあります:
1. リテラルを使用して直接代入する:
numbers := [5]int{1, 2, 3, 4, 5}これにより、numbers 配列の要素が順番に 1 から 5 で初期化されます。
2. 特定のインデックス位置の要素のみ指定する:
numbers := [5]int{0: 10, 4: 50}これにより、最初の要素(インデックス 0)が 10、最後の要素(インデックス 4)が 50 に設定されます。指定されていない残りの要素はゼロ値 (0) のままです。
3. ... を使用してコンパイラに長さを推論させる:
numbers := [...]int{1, 2, 3, 4, 5}ここでの ... はコンパイラに「波括弧内の要素の数に基づいて、自動的に配列の長さを計算してください」と伝えています。
1.3 配列要素へのアクセスと変更
インデックス (Index) を使用して配列の要素にアクセスします。インデックスは 0 から始まります:
numbers := [5]int{10, 20, 30, 40, 50}
fmt.Println(numbers[0]) // 出力: 10
fmt.Println(numbers[3]) // 出力: 40同様に、インデックスを介して対応する位置の値を変更できます:
numbers[1] = 25
fmt.Println(numbers[1]) // 出力: 251.4 配列の長さ
配列の長さは固定されており、組み込み関数 len() を使用して取得できます:
numbers := [5]int{1, 2, 3, 4, 5}
length := len(numbers)
fmt.Println(length) // 出力: 5注意: Go言語において、配列には長さから独立した「キャパシティ(容量)」という概念はありません。
1.5 核心的な重要ポイント:配列は「値型」 (Values) である
これはGo言語の配列における極めて重要な特性です:配列は値型です。 ある配列を別の配列変数に代入したり、関数の引数として渡したりすると、Goは配列の内容全体を完全にコピーします。コピー先(レプリカ)への変更は、元の配列には一切影響を与えません。
numbers1 := [3]int{1, 2, 3}
numbers2 := numbers1 // numbers2 は numbers1 の完全なコピーを取得
numbers2[0] = 100
fmt.Println(numbers1) // 出力: [1 2 3] (元の配列は無傷)
fmt.Println(numbers2) // 出力: [100 2 3]これは、JavaやPythonのように通常は参照やポインタを渡す他の多くのプログラミング言語とは論理が全く異なるため、必ず覚えておいてください!
2. Go言語におけるスライス (Slices)
配列は長さが固定されており、受け渡し時に全データがコピーされるため、実際の開発では柔軟性に欠けることが多々あります。そこで登場するのが スライス (Slices) です。スライスは、データシーケンスを扱うためのより強力で柔軟な方法を提供します。
スライスは本質的に、背後にある配列(基底配列)の連続した断片に対する「デスクリプタ (Descriptor)」です。内部には以下の3つの重要な情報が含まれています:
- 基底配列へのポインタ。
- スライスの長さ (Length)。
- スライスのキャパシティ (Capacity)。
2.1 スライスの宣言
スライスの宣言構文は配列に似ていますが、長さを指定しません:
var sliceName []dataType例えば、整数のスライスを宣言する場合:
var numbers []int2.2 スライスの作成
スライスは、主に以下の方法で作成できます:
1. 既存の配列から切り出す (Slicing):
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:4] // インデックス 1 (含む) からインデックス 4 (含まない) までを切り出し
fmt.Println(slice) // 出力: [2 3 4]これは元の array の一部を「見ている」スライスを作成します。
2. make() 関数を使用して動的に作成する:
slice := make([]int, 5) // 長さ 5、キャパシティ 5 のスライスを作成
slice2 := make([]int, 5, 10) // 長さ 5、キャパシティ 10 のスライスを作成make() 関数は自動的にメモリ内に隠れた基底配列を割り当て、それを指すスライスを返します。第1引数はスライスの型、第2引数は長さ、第3引数(任意)はキャパシティです。第3引数を省略した場合、キャパシティはデフォルトで長さと同じになります。
3. スライスリテラルを使用する (最も一般的):
slice := []int{1, 2, 3, 4, 5}この書き方は配列リテラルに似ていますが、括弧内に数字や ... がありません。Goは自動的に基底配列を作成し、それらの値を含むスライスを返します。
2.3 長さ (Length) と キャパシティ (Capacity)
- 長さ (Length): スライスが現在実際に含んでいる要素の数。
- キャパシティ (Capacity): スライスの最初の要素から数えて、基底配列の末尾までに存在する要素の総数。
それぞれ len() 関数と cap() 関数を使用して取得できます:
slice := make([]int, 3, 5)
fmt.Println(len(slice)) // 出力: 3
fmt.Println(cap(slice)) // 出力: 52.4 要素の動的な追加 (Append)
スライスは動的であり、いつでも新しい要素を追加できます。Goはこれを実現するために組み込み関数 append() を提供しています:
slice := []int{1, 2, 3}
slice = append(slice, 4, 5)
fmt.Println(slice) // 出力: [1 2 3 4 5]リサイズメカニズム: もし現在のキャパシティ (Capacity) が新しく追加する要素を収容するのに不足している場合、Goは自動的にメモリ内により大きな新しい配列を確保し、古いデータをコピーしてから新しい要素を追加し、最終的にスライスをこの全く新しい基底配列に向け直します。
2.5 スライスの再スライス (Slicing Slices)
既存のスライスに基づいて、さらに新しいスライスを切り出すことができます:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:4] // インデックス 1 から 4 まで
fmt.Println(slice2) // 出力: [2 3 4]極めて危険な罠: 複数のスライスが 同じ基底配列を共有している可能性がある ため、一つのスライスの要素を変更すると、他のスライスにも影響が及ぶことがよくあります!
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:4]
slice2[0] = 100 // slice2 の最初の要素を変更
fmt.Println(slice1) // 出力: [1 100 3 4 5] (slice1 も変更されてしまった!)
fmt.Println(slice2) // 出力: [100 3 4]ただし、もし append() によってリサイズ(新しい基底配列の生成)が発生した場合、それらは「別々の道」を歩むことになり、それ以降の変更は互いに影響しなくなります。
2.6 スライスの独立したコピー (Copying Slices)
元のスライスと完全に独立し、互いに干渉しないコピーを作成したい場合は、組み込み関数 copy() を使用する必要があります:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice1)) // まず十分な長さのメモリを割り当てる必要がある
copy(slice2, slice1) // slice1 の内容を slice2 にコピー
slice2[0] = 100 // コピー側を変更
fmt.Println(slice1) // 出力: [1 2 3 4 5] (元のスライスは影響を受けない)
fmt.Println(slice2) // 出力: [100 2 3 4 5]copy() 関数がコピーする要素の数は、2つのスライスのうち短い方の長さに依存します。
2.7 スライスのゼロ値 (Zero Value)
スライスのゼロ値は nil です。nil スライスの長さとキャパシティは共に 0 であり、いかなる基底配列も指していません。
var slice []int
fmt.Println(slice == nil) // 出力: true
fmt.Println(len(slice)) // 出力: 0
fmt.Println(cap(slice)) // 出力: 0しかし心配はいりません。Goは非常にスマートです。nil スライスに対して直接 append() を使用することができ、Goが自動的に基底配列を割り当ててくれます。
3. 配列 vs スライス:どちらを選ぶべきか?
| 特性 | 配列 (Array) | スライス (Slice) |
|---|---|---|
| 長さ | 固定(コンパイル時に確定) | 動的(実行時に拡張・縮小可能) |
| メモリ挙動 | 値型(代入時に全データをコピー) | 参照型(内部はポインタ構造) |
| 適用シーン | データサイズが確定しており不変な場合、極限のパフォーマンスが求められる低レイヤー | ほとんどの日常的な開発、動的なリスト操作が必要な場合 |
結論: Go言語において、99% のケースでは スライス を使用すべきです。
4. 実戦的なコード例
実際のプログラミングにおけるスライスの威力を見てみましょう。
4.1 数値グループの平均値を計算する
package main
import "fmt"
func main() {
numbers := []float64{10.5, 20.7, 30.2, 40.9, 50.1}
sum := 0.0
// range を使用してスライスを反復処理、'_' はインデックスを無視することを意味する
for _, number := range numbers {
sum += number
}
average := sum / float64(len(numbers))
fmt.Printf("平均値: %.2f\n", average) // 出力: 平均値: 30.50
}4.2 すべての偶数をフィルタリングする
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evenNumbers := []int{} // 空のスライスを初期化
for _, number := range numbers {
if number%2 == 0 {
// append を利用して要素を動的に追加
evenNumbers = append(evenNumbers, number)
}
}
fmt.Println("すべての偶数:", evenNumbers) // 出力: すべての偶数: [2 4 6 8 10]
}4.3 スライスを利用した「スタック」 (Stack) データ構造の実装
package main
import "fmt"
type Stack struct {
data []int
}
// Push: 入札、要素をスライスの末尾に追加
func (s *Stack) Push(value int) {
s.data = append(s.data, value)
}
// Pop: 出札、スライスの末尾の要素を取り出す
func (s *Stack) Pop() (int, bool) {
if len(s.data) == 0 {
return 0, false // スタックが空
}
index := len(s.data) - 1
value := s.data[index]
s.data = s.data[:index] // スライスを再スライスして、最後の要素を削除
return value, true
}
func main() {
stack := Stack{data: []int{}}
stack.Push(10)
stack.Push(20)
stack.Push(30)
value, ok := stack.Pop()
if ok {
fmt.Println("ポップされた要素:", value) // 出力: ポップされた要素: 30
}
value, ok = stack.Pop()
if ok {
fmt.Println("ポップされた要素:", value) // 出力: ポップされた要素: 20
}
}