Go言語(Golang)を効率的に使いこなす上で、配列(Array)とスライス(Slice)の違いを正しく理解することは非常に重要です。

多くの開発者が最初に直面する壁の一つが、この二つのデータ構造の使い分けやメモリ上での振る舞いの違いです。

一見すると似ているこれらは、内部的な仕組みや用途が大きく異なり、パフォーマンスに直結する要素を含んでいます。

本記事では、2026年現在のモダンなGoプログラミングにおける最適なプラクティスを交えながら、配列とスライスの核心に迫ります。

メモリ効率を最大化し、バグの少ないコードを書くためのポイントを詳しく見ていきましょう。

Go言語における配列(Array)の本質

Go言語の配列は、同じ型の要素を固定の長さで並べたデータ構造です。

最大の特徴は、配列の長さがその型の一部であるという点にあります。

例えば、[3]int[5]int は、Goの型システム上では全く別の型として扱われます。

配列は宣言時にサイズが確定し、その後変更することはできません。

また、配列を変数に代入したり、関数の引数として渡したりする場合、値のコピーが発生することに注意が必要です。

これは、大きな配列を関数に渡すと、スタックメモリの消費量が増え、パフォーマンス低下の原因になる可能性があることを意味します。

配列の宣言と初期化

配列を宣言するには、要素数と型を指定します。

Go
package main

import "fmt"

func main() {
    // 要素数5のint型配列を宣言(ゼロ値で初期化)
    var a [5]int
    fmt.Println("初期状態:", a)

    // 初期値を指定して宣言
    b := [3]string{"Go", "Python", "Rust"}
    fmt.Println("初期化済み:", b)

    // 要素数をコンパイラに推論させる
    c := [...]int{10, 20, 30, 40}
    fmt.Printf("長さ: %d, 値: %v\n", len(c), c)
}
実行結果
初期状態: [0 0 0 0 0]
初期化済み: [Go Python Rust]
長さ: 4, 値: [10 20 30 40]

このように、配列は固定長であるがゆえに、厳密なメモリレイアウトが求められる低レイヤの処理や、要素数が絶対に変わらないデータ(例えば暗号化ハッシュ値など)に適しています。

Go言語におけるスライス(Slice)の柔軟性

スライスは、配列の上に構築された動的なサイズ変更が可能なデータ構造です。

実際のGoプログラミングにおいて、リスト形式のデータを扱う場合は、ほとんどのケースでスライスが使用されます。

スライスは内部的に「バッキング配列(Underlying Array)へのポインタ」「長さ(Length)」「容量(Capacity)」の3つのフィールドを持つ構造体として実装されています。

そのため、スライスを関数に渡す際は、背後にある大きな配列データそのものではなく、小さな構造体情報のみがコピーされます

これにより、大量のデータを効率的に受け渡しできるのがスライスの大きな強みです。

スライスの宣言と操作

スライスは、要素数を指定せずに宣言するか、make 関数を使用して作成します。

Go
package main

import "fmt"

func main() {
    // スライスの宣言
    var s []int
    fmt.Printf("nilスライス: len=%d cap=%d %v\n", len(s), cap(s), s)

    // make関数による作成(長さ3, 容量5)
    s2 := make([]int, 3, 5)
    fmt.Printf("make作成: len=%d cap=%d %v\n", len(s2), cap(s2), s2)

    // 要素の追加
    s2 = append(s2, 10, 20)
    fmt.Printf("追加後: len=%d cap=%d %v\n", len(s2), cap(s2), s2)

    // 容量を超える追加(自動的な再割り当て)
    s2 = append(s2, 30)
    fmt.Printf("容量超え後: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
}
実行結果
nilスライス: len=0 cap=0 []
make作成: len=3 cap=5 [0 0 0]
追加後: len=5 cap=5 [0 0 0 10 20]
容量超え後: len=6 cap=10 [0 0 0 10 20 30]

append 関数を使用すると、必要に応じて自動的に新しいバッキング配列が確保され、容量が拡張されます。

この挙動こそが、スライスを「可変長配列」のように扱える理由です。

配列とスライスの主要な違い

ここでは、配列とスライスの決定的な違いをいくつかの観点で整理します。

1. 型の定義

配列は [N]T 型であり、N(サイズ)が異なれば別の型になります。

スライスは []T 型であり、要素数に関わらず同じ型として扱われます。

2. メモリの割り当て方式

配列はスタックに割り当てられることが多いですが、スライス(のバッキング配列)はヒープに割り当てられる可能性が高くなります。

ただし、小さな配列やエスケープ解析によって最適化されたスライスは、この限りではありません。

3. 関数への渡し方(値渡し vs 参照的な渡し)

配列を関数に渡すと全要素がコピーされます。

スライスを関数に渡すと、バッキング配列への参照情報のみがコピーされます。

したがって、関数内でスライスの要素を書き換えると、呼び出し元のスライスが参照しているバッキング配列の中身も変更されます。

比較表:配列 vs スライス

それぞれの特性を一覧表にまとめました。

項目配列 (Array)スライス (Slice)
サイズ固定(コンパイル時に決定)可変(実行時に動的変更)
型の定義[size]T[]T
代入/引数渡し値の全コピーポインタ・長さ・容量のコピー
主な用途固定長バッファ、数学的ベクトル一般的なリスト、ストリーム処理
組み込み関数len()len(), cap(), append(), copy()

メモリ効率を意識したスライスの使い方

Goにおいてパフォーマンスを追求する場合、スライスの「容量(Capacity)」を意識することが不可欠です。

スライスの容量が不足した状態で append を繰り返すと、背後で新しい配列の確保と既存データのコピーが何度も発生します。

これは、ガベージコレクション(GC)の負荷を高め、実行速度を低下させる要因になります。

make関数による事前キャパシティ確保

あらかじめ必要な要素数が予測できる場合は、make の第3引数で容量を指定してください。

Go
// 悪い例:ループのたびに再割り当てが発生する可能性がある
data := []int{}
for i := 0; i < 1000; i++ {
    data = append(data, i)
}

// 良い例:最初に容量を確保しておく
data := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    data = append(data, i)
}

このように、事前に容量を確保することで、メモリアロケーションの回数を劇的に減らすことが可能です。

大きな配列の部分スライスによるメモリリーク

スライスを使用する際の落とし穴として、「メモリの残り続け」問題があります。

非常に大きな配列から、ほんの数要素だけをスライスして保持し続ける場合、背後にある巨大なバッキング配列全体がメモリ上に残り続けます。

これを防ぐためには、必要な部分だけを新しいスライスに copy する手法が有効です。

Go
// メモリを大量に消費し続ける可能性がある例
func getSmallPart(bigData []byte) []byte {
    return bigData[0:10]
}

// メモリ効率が良い例
func getSmallPartOptimized(bigData []byte) []byte {
    smallPart := make([]byte, 10)
    copy(smallPart, bigData[0:10])
    return smallPart
}

copy 関数を使用することで、新しいバッキング配列が作成され、元の巨大な bigData はGCの対象となります。

使い分けの判断基準

開発の現場では、どちらを使うべきか迷うことがあるかもしれません。

基本的には、「迷ったらスライスを使う」のがGoの標準的なスタイルです。

しかし、明確な使い分けの基準を持っておくことは、より堅牢な設計に繋がります。

配列を使用すべきケース

  • データのサイズが数学的または仕様的に固定されている場合(例:IPv4アドレスの4バイト、SHA-256の32バイト)。
  • データをスタックに配置させ、ヒープ割り当てを一切排除したい極限の最適化が必要な場合。
  • 値としての不変性を重視し、関数に渡した際に絶対に元の値が変更されないことを保証したい場合。

スライスを使用すべきケース

  • Web APIのレスポンスやデータベースのクエリ結果など、件数が動的に変わるデータを扱う場合。
  • 標準ライブラリ(io, sort, jsonなど)のほとんどの関数がスライスを要求するため、それらとの親和性を保ちたい場合。
  • メモリの効率的な共有を行い、不要なコピーを避けたい場合。

Go 1.20以降の配列ポインタからのスライス変換

近年のGoのアップデートでは、配列とスライスの連携がさらに強化されています。

Go 1.20からは、配列へのポインタを直接スライスに変換する記述がより簡潔に行えるようになりました。

これにより、特定の固定長バッファをスライスとしてAPIに渡す際の柔軟性が向上しています。

Go
arr := [4]int{1, 2, 3, 4}
// 配列へのポインタからスライスへの変換
slice := arr[:]

このような言語仕様の進化を追いかけることで、古い手法に縛られないモダンなコーディングが可能になります。

2026年の現在でも、Goのコアコンセプトである「シンプルさ」は変わりませんが、こうした細かな最適化手法を知っているかどうかが、エンジニアとしての実力差に繋がります。

まとめ

Go言語の配列とスライスは、一見似て非なるものです。

配列は「固定長の型」であり、値のコピーが基本となる静的なデータ構造です。

一方、スライスは「バッキング配列への参照」を持ち、動的な拡張や効率的なデータ共有を可能にするGoの主役級データ構造です。

メモリ効率を最大化するためには、スライスの容量(capacity)を意識し、必要に応じて makecopy を使い分けることが重要です。

基本的には柔軟なスライスを使いつつ、データの整合性や特殊なパフォーマンス要件がある場合に配列を選択するという戦略が、最も合理的と言えるでしょう。

本記事で紹介した仕組みと使い分けのポイントを意識して、よりクリーンで効率的なGoプログラムを記述していきましょう。