Goの固定長シーケンス:配列をマスターする
Wenhao Wang
Dev Intern · Leapcell

コンピュータサイエンスの世界では、データ構造は基本的な構成要素です。その中でも、シーケンス、つまり要素の順序付けられたコレクションという概念は非常に重要です。このシーケンスの要素数が事前に決定されており、変更できない場合、それを固定長シーケンスと呼びます。Goでは、この特定の種類のシーケンスは配列によって具現化されます。
Goのslice
型は、その動的な性質と一般的な使用法で脚光を浴びがちですが、array
を理解することは不可欠です。配列はスライスが構築される基盤となる構造であり、特定のシナリオに適した独自の特性を提供します。この記事では、Goにおける配列の性質を掘り下げ、その定義、動作、および実用的な応用を探り、より柔軟な対比であるスライスとの違いを明確にします。
Goにおける配列とは
Goにおける配列とは、同じ型のゼロ個以上の要素からなる固定長のシーケンスであり、メモリ上に連続して格納されます。 「固定長」という側面が、その定義的な特徴です。配列が宣言されたサイズを持つと、そのサイズを変更することはできません。
この定義を分解してみましょう。
- 固定長: 要素の数は配列の型の一部です。たとえば、
[5]int
は[10]int
とは異なる型です。このサイズの不変性が、スライスとの重要な差別化要因です。 - シーケンス: 要素は順序付けられており、定義された位置(インデックス)を持ちます。最初の要素はインデックス0、2番目の要素はインデックス1、と続き、
length - 1
までです。 - 同じ型: 配列内のすべての要素は、同一のデータ型(例:すべて
int
、すべてstring
、すべてfloat64
、またはカスタム構造体のすべてインスタンス)である必要があります。 - 連続したメモリ: 配列要素はメモリ上で連続して格納されます。この連続した割り当てにより、要素のメモリアドレスを直接計算できるため、インデックスを介した要素への効率的なアクセスが可能になります。
配列の宣言と初期化
配列は、その長さと要素の型を指定して宣言します。いくつか例を見てみましょう。
package main import "fmt" func main() { // 5つの整数の配列、ゼロ値(intの場合は0)で初期化 var a [5]int fmt.Println("Declared array 'a':", a) // 出力: [0 0 0 0 0] // 3つの文字列の配列、初期値付き var b [3]string = [3]string{"apple", "banana", "cherry"} fmt.Println("Declared array 'b':", b) // 出力: [apple banana cherry] // 配列の短縮宣言と初期化 c := [4]float64{1.1, 2.2, 3.3, 4.4} fmt.Println("Declared array 'c':", c) // 出力: [1.1 2.2 3.3 4.4] // "..."を使用してコンパイラに要素数を数えさせる d := [...]bool{true, false, true} // 長さは3と推論される fmt.Println("Declared array 'd':", d) // 出力: [true false true] fmt.Printf("Type of 'd': %T\n", d) // 出力: Type of 'd': [3]bool // 要素へのアクセス fmt.Println("First element of 'b':", b[0]) // 出力: apple fmt.Println("Last element of 'c':", c[len(c)-1]) // 出力: 4.4 // 要素の変更 a[0] = 10 a[4] = 50 fmt.Println("Modified array 'a':", a) // 出力: [10 0 0 0 50] }
明示的に配列を初期化しない場合、その要素はそれぞれのゼロ値(例:数値型の場合は0
、文字列の場合は""
、ブール値の場合はfalse
、ポインタの場合はnil
)に設定されることに注意してください。
配列とスライス:決定的な違い
ここで、Goの初心者の間で混乱が生じることが多いです。配列とスライスはどちらも要素のシーケンスを表しますが、それらの根本的な違いは、長さと基盤となるメカニズムにあります。
特徴 | 配列 ([N]T ) | スライス ([]T ) |
---|---|---|
長さ | 宣言時に固定(型の構成要素) | 動的、成長または縮小可能(append を使用) |
型 | [N]T (例:[5]int ) | []T (例:[]int ) |
値セマンティクス | 値型:代入は配列全体をコピーする。 | 参照型:代入はスライスヘッダーをコピーする(基盤となる配列セグメントを指す)。 |
関数への受け渡し | 値渡し(配列のコピーを作成する)。 | 参照渡し(スライスヘッダーがコピーされるが、基盤となるデータを指す same)。 |
基盤となる構造 | N 個の要素のための連続したメモリブロック。 | 基盤となる配列へのポインタ、長さ、および容量を含むヘッダー。 |
配列の値セマンティクスの意味合いを考えてみましょう。
package main import "fmt" func modifyArray(arr [3]int) { arr[0] = 99 // これは配列の*コピー*を変更します fmt.Println("Inside function (copied array):", arr) } func main() { originalArray := [3]int{1, 2, 3} fmt.Println("Original array before function call:", originalArray) modifyArray(originalArray) fmt.Println("Original array after function call:", originalArray) // まだ [1 2 3] のままです // 比較のためにスライスを使用 originalSlice := []int{1, 2, 3} fmt.Println("Original slice before function call:", originalSlice) // 配列のスライスを作成するとスライスが作成されます mySlice := originalArray[:] // mySlice は originalArray を参照するスライスです mySlice[0] = 100 // これは originalArray の基盤となるデータをスライス経由で*変更*します fmt.Println("Original array after slice modification:", originalArray) // [100 2 3]になります }
originalArray
がmodifyArray
に渡されると、3つの整数配列の完全なコピーが作成されます。modifyArray
内の変更は、このローカルコピーのみに影響します。これは、大きな配列の場合、メモリを大量に消費する可能性があります。
対照的に、mySlice
がoriginalArray
から作成されると、mySlice
はoriginalArray
のデータ全体を指すスライスヘッダーになります。mySlice
を介して要素を変更すると、originalArray
の要素が直接変更されます。これが、スライスが動的なデータ処理によく好まれる理由です。高価なフルデータコピーを回避するためです。
配列を使用するタイミング
スライスの普及と柔軟性を考えると、配列が常に適切な選択肢であると疑問に思うかもしれません。配列は、その固定長と値セマンティクスが有益または必須である特定のシナリオで輝きます。
-
固定サイズのバッファ/データ構造: コレクションの正確な最大サイズが事前にわかっており、それが変更されない場合。
- 例: RGBカラー値(
[3]uint8
)、IPv4アドレス([4]byte
)、またはGPS座標([2]float64
)の格納。これらは本来固定です。
type RGB struct { R uint8 G uint8 B uint8 } func main() { var redColor RGB = RGB{255, 0, 0} fmt.Printf("Red Color: R=%d, G=%d, B=%d\n", redColor.R, redColor.G, redColor.B) // 固定サイズである2Dグリッド/行列用の配列の使用 var matrix [2][3]int // 2x3行列 matrix[0] = [3]int{1, 2, 3} matrix[1] = [3]int{4, 5, 6} fmt.Println("Matrix:", matrix) }
- 例: RGBカラー値(
-
パフォーマンス最適化(マイクロ最適化): 極めてパフォーマンスが重視されるコードでは、スライスのオーバーヘッド(ヘッダー、容量チェック、潜在的な再割り当て)を回避することで、わずかなメリットが得られることがあります。ただし、Goコンパイラとランタイムはスライスに対して高度に最適化されているため、これはめったにない主な理由です。
-
Cとの相互運用: cgoを介してCライブラリとやり取りする場合、配列はCスタイルの固定サイズ配列の直接的な同等物であることがよくあります。
-
マップのキー(まれ!):配列は値型であり、要素が比較可能であれば比較可能であるため、マップのキーとして使用できることがあります。スライスは参照型であるため、マップのキーにはなれません。
package main import "fmt" func main() { // マップのキーとしての配列 counts := make(map[[3]int]int) point1 := [3]int{1, 2, 3} point2 := [3]int{1, 2, 3} // point1と同じ値 point3 := [3]int{4, 5, 6} counts[point1] = 1 counts[point3] = 10 fmt.Println("Count for point1:", counts[point1]) fmt.Println("Count for point2 (same value):", counts[point2]) // 1が出力されます fmt.Println("Count for point3:", counts[point3]) }
この例は、
[3]int{1, 2, 3}
と[3]int{1, 2, 3}
がマップのキーとして同等と見なされることを示しています。これは、配列が値型であり、その内容が比較されるためです。 -
スライスの基盤となるデータ: 前述したように、すべてのスライスは内部的に基盤となる配列を参照します。スライスを作成するときは、新しい基盤となる配列を作成するか、既存の配列の一部を参照します。たとえば、
arr[:]
はarr
全体を参照するスライスを作成します。
関数シグネチャにおける配列
値セマンティクスによる配列の関数パラメータでの扱われ方を理解することは重要です。
package main import "fmt" // この関数は正確に5つの整数の配列を受け取ります。 // この関数を呼び出すと配列のコピーが作成されます。 func processFixedArray(data [5]int) { fmt.Println("Inside processFixedArray (before modification):", data) data[0] = 999 // ローカルコピーを変更します fmt.Println("Inside processFixedArray (after modification):", data) } // この関数は整数のスライスを受け取ります。 // スライスヘッダがコピーされますが、それは同じ基盤となるデータを指します。 func processSlice(data []int) { fmt.Println("Inside processSlice (before modification):", data) if len(data) > 0 { data[0] = 999 // 実際の基盤となるデータを変更します } fmt.Println("Inside processSlice (after modification):", data) } func main() { myArray := [5]int{10, 20, 30, 40, 50} fmt.Println("Original array before processFixedArray:", myArray) processFixedArray(myArray) fmt.Println("Original array after processFixedArray:", myArray) // 変更されない: [10 20 30 40 50] // スライスベースの関数で配列の内容を処理するには、 // 通常、配列から派生したスライスを渡します。 mySlice := myArray[:] // 配列からスライスを作成 fmt.Println("\nOriginal array before processSlice:", myArray) processSlice(mySlice) fmt.Println("Original array after processSlice:", myArray) // 変更される: [999 20 30 40 50] // サイズの異なる配列を渡そうとするとどうなりますか? // var smallArray [3]int = {1,2,3} // processFixedArray(smallArray) // コンパイル時エラー: cannot use smallArray (variable of type [3]int) as type [5]int // 配列が期待される場所にスライスを渡そうとするとどうなりますか? // var dynamicSlice []int = []int{1,2,3,4,5} // processFixedArray(dynamicSlice) // コンパイル時エラー: cannot use dynamicSlice (variable of type []int) as type [5]int }
この例は、processFixedArray
がコピーで動作するのに対し、processSlice
はスライスヘッダを介して基盤となるデータで動作することが明確に示されています。配列の強力な型付けにより、サイズN
の配列をサイズM
(N != M
)を期待する関数に渡すことはできません。この厳密さは、「固定長」が配列の型の一部であることを強調しています。
結論
Goの配列は、一般的なデータコレクションにはスライスほど頻繁に使用されませんが、基本的です。それらは、同じ型の要素の固定長、連続したシーケンスを表します。その主な特性—固定サイズ、値セマンティクス、および格納の連続性—は、コレクションの正確な境界がわかっていて変更できない場合、または直接のメモリレイアウトと型レベルのサイズ保証が有益である場合に理想的です。
Go配列のユニークな性質、特にスライスとの対比を理解することは、効率的で正確で、Goらしいコードを書くために不可欠です。スライスは動的な柔軟性を提供しますが、配列は特定のプログラミング課題に適したコンパイル時保証と直接性を提供します。両方を効果的に活用することで、Go開発者はデータの固有のプロパティに合わせて調整された、堅牢でパフォーマンスの高いアプリケーションを作成できます。