Go言語(Golang)を学習する際、データの集合を扱うための構造として「配列」と「スライス」の理解は避けて通れません。

特に多次元的なデータを扱う場合、どのようにメモリを確保し、効率的にアクセスするかを知ることはプログラムのパフォーマンスに直結します。

Go言語の多次元配列は、行列計算やグリッドベースのゲーム開発、さらには複雑なデータ構造を表現する際に非常に重要な役割を果たします。

本記事では、Go言語における多次元配列の基礎的な宣言方法から、実務で多用されるスライスとの違いまでを詳しく解説します。

配列の仕組みを正しく理解することで、より堅牢で効率的なコードを書くための土台を築いていきましょう。

Go言語における多次元配列の基本概念

Go言語における多次元配列とは、配列の要素自体がさらに配列である構造を指します。

例えば、2次元配列は「配列の配列」であり、数学における行列のようなイメージで捉えると理解しやすいでしょう。

Go言語の配列は、宣言時にその長さが固定されるという特徴を持っています。

一度宣言した配列のサイズを後から変更することはできないため、多次元配列を設計する際もサイズ設計が重要になります。

また、Go言語では配列の「サイズ」もその型の一部として扱われます。

例えば、[3][3]int[4][4]int は、たとえベースとなる型が同じ int であっても、言語仕様上は全く異なる型として扱われる点に注意してください。

多次元配列の宣言と初期化方法

Go言語で多次元配列を宣言する方法には、いくつかのパターンが存在します。

開発の状況に応じて、最も適切な初期化方法を選択することがコードの可読性を高める鍵となります。

変数の宣言とデフォルト値による初期化

まずは、最もシンプルな変数の宣言方法を見ていきましょう。

Go言語では変数を宣言すると、その型に応じた「ゼロ値」で自動的に初期化されます。

Go
package main

import "fmt"

func main() {
    // 3行2列のint型2次元配列を宣言
    var matrix [3][2]int

    // 中身を表示(すべて0で初期化される)
    fmt.Println(matrix)
}
実行結果
[[0 0] [0 0] [0 0]]

数値型の場合、ゼロ値は 0 であるため、上記のコードではすべての要素が 0 で埋められた配列が作成されます。

配列リテラルを用いた初期化

宣言と同時に特定の値を代入したい場合は、配列リテラルを使用します。

この方法は、初期データが決まっている定数のようなデータを定義する際に便利です。

Go
package main

import "fmt"

func main() {
    // 宣言と同時に値を代入
    matrix := [2][3]int{
        {1, 2, 3},
        {4, 5, 6},
    }

    fmt.Println(matrix)
}
実行結果
[[1 2 3] [4 5 6]]

外側の波括弧の中に、さらに内側の配列を定義する形式で記述します。

また、最後の要素の後にもカンマ , を付けるのがGo言語の構文上のルールです。

要素へのアクセスと値の更新

多次元配列の特定の要素にアクセスするには、インデックスを指定します。

インデックスは 0 から始まるため、n 番目の要素にアクセスしたい場合は n-1 を指定します。

Go
package main

import "fmt"

func main() {
    var board [3][3]string

    // 特定のインデックスに値を代入
    board[1][1] = "X"
    board[0][2] = "O"

    fmt.Println("1行1列目の値:", board[1][1])
    fmt.Println("配列全体:", board)
}
実行結果
1行1列目の値: X
配列全体: [[  O] [ X ] [  ]]

多次元配列では、配列名[行][列] のように複数のブラケットを重ねて記述します。

範囲外のインデックスを指定しようとすると、コンパイルエラーまたは実行時エラーが発生するため注意が必要です。

多重ループを用いた要素の反復処理

多次元配列のすべての要素を処理する場合、for ループを入れ子にするのが一般的です。

Go言語では range キーワードを使用することで、より安全かつ簡潔にループを記述できます。

Go
package main

import "fmt"

func main() {
    matrix := [2][2]int{{10, 20}, {30, 40}}

    // 外側のループで行を取り出す
    for i, row := range matrix {
        // 内側のループで列の値を取り出す
        for j, value := range row {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, value)
        }
    }
}
実行結果
matrix[0][0] = 10
matrix[0][1] = 20
matrix[1][0] = 30
matrix[1][1] = 40

range を使用すると、インデックスと要素のコピーを同時に取得できます。

もしインデックスが不要な場合は、ブランク識別子 _ を使って無視することも可能です。

配列とスライスの決定的な違い

Go言語で開発を行う上で、配列とスライスの違いを理解することは極めて重要です。

実務では、純粋な配列よりも「多次元スライス(スライスのスライス)」が利用されるケースが圧倒的に多いです。

固定長か可変長か

最大の相違点は、そのサイズが固定されているかどうかです。

配列は宣言時に決めたサイズを一生変えることができません。

一方、スライスは append 関数を使用することで、動的に要素を追加してサイズを拡張できます。

メモリ効率と値渡し・参照渡し

Go言語において、配列は「値型」です。

関数に配列を渡すと、配列全体がメモリ上でコピーされるため、大きな多次元配列を渡す場合はパフォーマンスが低下する恐れがあります。

対してスライスは内部的に配列へのポインタを持つ「参照型」のような振る舞いをします。

多次元データを関数間で頻繁に受け渡しする場合は、スライスを使用するのがベストプラクティスです。

比較表:配列 vs スライス

特徴配列スライス
サイズ固定(型の一部)可変(動的に変更可能)
メモリ渡し値渡し(コピーが発生)参照渡し(ポインタをコピー)
宣言例[3][3]int[][]int

多次元スライスの動的な作成

多次元スライスを扱う場合、内側のスライスも個別に初期化する必要があります。

make 関数を使用して、動的に2次元スライスを生成する例を見てみましょう。

Go
package main

import "fmt"

func main() {
    rows := 3
    cols := 2

    // 外側のスライスを作成
    matrix := make([][]int, rows)

    for i := range matrix {
        // 各行のスライスを個別に作成
        matrix[i] = make([]int, cols)
        for j := range matrix[i] {
            matrix[i][j] = i + j
        }
    }

    fmt.Println(matrix)
}
実行結果
[[0 1] [1 2] [2 3]]

このように、多次元スライスは「可変長の行の中に、さらに可変長の列がある」という柔軟な構造を持たせることができます。

行ごとに列の長さが異なる「ジャグ配列(不揃いな配列)」を作成することも可能です。

実践的なコード例:3次元配列によるデータの表現

多次元配列は2次元に留まりません。

例えば、時間軸を含めた3次元空間のデータを扱う際には、3次元配列が適しています。

Go
package main

import "fmt"

func main() {
    // 2x2x2の3次元配列
    var cube [2][2][2]int

    // 値をセット
    cube[0][0][0] = 1
    cube[1][1][1] = 8

    fmt.Println(cube)
}
実行結果
[[[1 0] [0 0]] [[0 0] [0 8]]]

3次元以上になると視覚的なイメージは難しくなりますが、プログラム上では単にインデックスが増えるだけです。

ただし、次元が増えるほどメモリ消費量が指数関数的に増大するため、設計には注意が必要です。

多次元データを扱う際のパフォーマンスのヒント

Go言語は高速な言語ですが、多次元データの扱い方一つでさらに速度を向上させることができます。

多次元配列をループで回す際は、「メモリの連続性」を意識することが重要です。

Goの配列はメモリ上に連続して配置されるため、内側のループで連続したメモリ領域にアクセスするように(行優先で)記述すると、CPUキャッシュの効率が良くなります。

また、非常に大きな多次元データを扱う場合、1次元の大きなスライスを用意し、インデックス計算(index = row * total_cols + col)によって疑似的に多次元を表現する手法も一般的です。

この手法は、メモリアロケーションの回数を減らし、ガベージコレクション(GC)の負荷を軽減する効果があります。

まとめ

Go言語における多次元配列は、データの構造化に欠かせない基本要素です。

サイズが固定される「配列」と、柔軟に拡張可能な「スライス」の性質を正しく理解し、用途に合わせて使い分けることが上達への近道です。

基本的には、メモリ効率や拡張性の観点からスライスのスライス(多次元スライス)を利用する場面が多くなるでしょう。

一方で、サイズが厳密に決まっている行列計算などでは、配列を使用することで型安全性を高めることができます。

今回学んだ宣言方法、初期化、そして反復処理のパターンを活用して、より高度なGoプログラムの開発に挑戦してみてください。