フリーキーズ | 独学プログラミング

Go言語の配列とスライスの違いと使い分け!初心者が混同しやすいポイントを解説

リンドくん

リンドくん

たなべ先生、Go言語で配列とスライスっていうのがあるみたいなんですけど、何が違うんですか?どっちも同じような気がして...

たなべ

たなべ

それは多くのGo初心者が混同してしまう部分なんだ。
配列は固定サイズ、スライスは動的サイズというのが基本的な違いなんだよ。でも、それだけじゃないから詳しく説明していくね。

Go言語を学び始めると、必ず出会うのが「配列(Array)」と「スライス(Slice)」という2つのデータ構造です。

一見すると似たような機能に見えるこの2つですが、実際には全く異なる特性を持っており、適切な使い分けがGo言語でのプログラミング効率を大きく左右します。
特に他のプログラミング言語から移ってきた方にとって、この違いは最初につまずきやすいポイントでもあります。

本記事では、Go言語における配列とスライスの基本的な違いから、実際の開発現場での使い分け方法まで、プログラミング初心者の方でも理解できるよう詳しく解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

配列とスライスの基本的な違い

リンドくん

リンドくん

先生、配列が固定サイズってどういうことですか?一度決めたら変えられないんですか?

たなべ

たなべ

その通り!配列は宣言時にサイズが決定されて、後から変更することはできないんだ。
一方でスライスは実行時にサイズを変更できる柔軟性があるよ。

Go言語における配列とスライスの最も重要な違いは、サイズの固定性にあります。

配列(Array)の特徴

配列はコンパイル時にサイズが確定する静的なデータ構造です。以下のような特徴があります。

  • サイズは宣言時に決定され、後から変更不可
  • メモリ上に連続して配置される
  • 値渡しされる(コピーが作成される)
  • 型の一部としてサイズが含まれる
// 配列の宣言と初期化
var numbers [5]int // 5個のint型要素を持つ配列
fruits := [3]string{"apple", "banana", "orange"} // 初期値付きで宣言

// サイズは型の一部
var array1 [3]int
var array2 [5]int
// array1とarray2は異なる型として扱われる

スライス(Slice)の特徴

スライスは動的に拡張可能なデータ構造で、配列よりもはるかに柔軟です。

  • サイズを実行時に変更可能
  • 内部的には配列への参照を保持
  • 参照渡しされる
  • 長さ(length)と容量(capacity)という2つの概念を持つ
// スライスの宣言と初期化
var numbers []int // 空のスライス
fruits := []string{"apple", "banana"} // 初期値付きで宣言

// 要素の追加(動的拡張)
fruits = append(fruits, "orange")
fmt.Println(fruits) // [apple banana orange]

この基本的な違いを理解することで、どちらを使うべきかの判断基準が見えてきます。
固定サイズでパフォーマンスを重視する場面では配列を、柔軟性とメモリ効率を重視する場面ではスライスを選ぶのが一般的です。

配列の使い方と特徴

配列は、事前にデータの個数が決まっている場合や、メモリ効率とパフォーマンスを最優先にしたい場合に威力を発揮します。

配列の宣言方法

// 1. サイズを指定して宣言
var scores [10]int

// 2. 初期値と一緒に宣言
temperatures := [7]float64{23.5, 25.1, 22.3, 26.8, 24.2, 23.9, 25.5}

// 3. 初期値の個数から自動でサイズ決定
weekdays := [...]string{"Mon", "Tue", "Wed", "Thu", "Fri"}

配列の特性とメリット

メモリ効率の良さ

配列はメモリ上に連続して配置されるため、CPUキャッシュの効率が良く、高速なアクセスが可能です。

型安全性

配列のサイズが型の一部となるため、異なるサイズの配列を誤って混在させるリスクをコンパイル時に発見できます。

func processScores(scores [10]int) {
    // 必ず10個の要素があることが保証される
    for i := 0; i < 10; i++ {
        fmt.Printf("Score %d: %d\n", i+1, scores[i])
    }
}

// 呼び出し例
var gameScores [10]int
processScores(gameScores) // OK

var testScores [5]int
// processScores(testScores) // コンパイルエラー!

配列の制限事項

サイズの固定性が最大の制約です。データ量が実行時に変わる可能性がある場合、配列では対応できません。
また、値渡しされるため、大きな配列を関数に渡すとコピーコストが発生します。

スライスの使い方と特徴

リンドくん

リンドくん

スライスって何か魔法みたいですね!どうやって大きさを変えているんですか?

たなべ

たなべ

いい着眼点だね!スライスは内部的に配列への参照と長さ、容量の情報を持っているんだ。
必要に応じて新しい配列を作って参照先を変更しているよ。

スライスはGo言語で最も頻繁に使用されるデータ構造の一つです。その柔軟性により、様々な場面で活用できます。

スライスの基本的な使い方

// 空のスライス作成
var numbers []int

// make関数でスライス作成
scores := make([]int, 5)        // 長さ5、容量5
buffer := make([]byte, 0, 100)  // 長さ0、容量100

// リテラルで初期化
fruits := []string{"apple", "banana", "cherry"}

// 要素の追加
fruits = append(fruits, "date", "elderberry")

スライスの内部構造

スライスは以下の3つの情報を持つ構造体として実装されています。

  • ポインタ → 実際のデータが格納された配列への参照
  • 長さ(length) → 現在格納されている要素数
  • 容量(capacity) → 参照先配列の最大サイズ
s := make([]int, 3, 5)
fmt.Printf("長さ: %d, 容量: %d\n", len(s), cap(s))
// 出力: 長さ: 3, 容量: 5

スライスの動的拡張メカニズム

スライスに要素を追加する際、容量が不足すると自動的に新しい配列が作成されます。

s := []int{1, 2, 3}
fmt.Printf("初期 - 長さ: %d, 容量: %d\n", len(s), cap(s))

s = append(s, 4, 5, 6, 7)
fmt.Printf("追加後 - 長さ: %d, 容量: %d\n", len(s), cap(s))
// 容量が自動的に拡張される

この動的拡張により、メモリを効率的に使いながら必要に応じてデータを格納できます。

スライスの部分抽出

スライスの強力な機能の一つが部分抽出(スライシング)です。

numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

// 部分抽出
subset1 := numbers[2:5]   // [2, 3, 4]
subset2 := numbers[:3]    // [0, 1, 2]
subset3 := numbers[7:]    // [7, 8, 9]

fmt.Println(subset1, subset2, subset3)

この機能により、データの一部を効率的に操作することが可能になります。

実際の開発における使い分け

実際のGo言語開発では、配列とスライスをどのように使い分けるべきでしょうか。ここでは実践的な判断基準を示します。

配列を選ぶべき場面

1. 固定長のデータ構造

// RGB色情報(必ず3つの値)
type Color [3]uint8

// 3次元座標(X, Y, Z必須)
type Point3D [3]float64

// 一週間の売上(7日固定)
type WeeklySales [7]float64

2. 高パフォーマンスが要求される処理

科学計算や組み込みシステムなど、メモリ効率とアクセス速度が重要な場面では配列の方が適しています。

3. コンパイル時の型安全性を重視する場合

異なるサイズのデータを混同するリスクを排除したい場合、配列の型システムが有効です。

スライスを選ぶべき場面

1. データ量が実行時に決定される場合

// ユーザー入力を格納
var userInputs []string

// APIレスポンスのデータ
type APIResponse struct {
    Items []Item `json:"items"`
}

// ファイル読み込み結果
lines, err := readFileLines("config.txt")

2. データの操作が頻繁な場合

// フィルタリング処理
func filterEvenNumbers(numbers []int) []int {
    var result []int
    for _, num := range numbers {
        if num%2 == 0 {
            result = append(result, num)
        }
    }
    return result
}

// ソート処理
import "sort"
names := []string{"charlie", "alice", "bob"}
sort.Strings(names)

3. 関数間でのデータ受け渡し

Go言語の標準ライブラリやサードパーティライブラリの多くはスライスを使用しているため、互換性の観点からもスライスが適しています。

パフォーマンスの考慮

現代の大規模Webアプリケーション開発では、メモリ効率も重要な判断基準です。

  • 小さな固定データ: 配列が効率的
  • 大量データの処理: スライスで必要な分だけメモリ確保
  • データ変換や加工: スライスの柔軟性が有利

よくある間違いとトラブルシューティング

リンドくん

リンドくん

先生、さっき自分でコード書いてたら、スライスのコピーで思った通りにならなかったんです...

たなべ

たなべ

あー、それはよくあるミスだね!
スライスは参照渡しだから、コピーのつもりでも元のデータが変更されちゃうことがあるんだ。対策方法を教えるよ。

Go言語の配列とスライスを使う際に、初心者が陥りやすい落とし穴とその解決方法を紹介します。

よくある間違い1 スライスの参照共有

// 間違った例
original := []int{1, 2, 3, 4, 5}
copied := original[1:4]  // [2, 3, 4]

// copiedを変更すると...
copied[0] = 999

fmt.Println(original)  // [1, 999, 3, 4, 5] - 元も変更される!

正しい解決方法

// 独立したコピーを作成
original := []int{1, 2, 3, 4, 5}
copied := make([]int, 3)
copy(copied, original[1:4])

copied[0] = 999
fmt.Println(original)  // [1, 2, 3, 4, 5] - 元は変更されない

よくある間違い2 ループ内でのスライス拡張

// 非効率な例
var result []int
for i := 0; i < 1000; i++ {
    result = append(result, i)  // 何度も容量拡張が発生
}

効率的な解決方法

// 事前に容量を確保
result := make([]int, 0, 1000)  // 容量1000で初期化
for i := 0; i < 1000; i++ {
    result = append(result, i)  // 容量拡張が発生しない
}

よくある間違い3 配列とスライスの混同

// 関数で異なるサイズの配列を受け取ろうとする
func processArray(arr [5]int) {  // 5個固定
    // 処理
}

var data1 [3]int
var data2 [5]int

processArray(data2)  // OK
// processArray(data1)  // コンパイルエラー

柔軟な解決方法

// スライスを使用することで任意のサイズに対応
func processSlice(slice []int) {
    // 任意のサイズのスライスを処理可能
    for i, val := range slice {
        fmt.Printf("Index %d: %d\n", i, val)
    }
}

var data1 []int = []int{1, 2, 3}
var data2 []int = []int{1, 2, 3, 4, 5}

processSlice(data1)  // OK
processSlice(data2)  // OK

デバッグのコツ

スライスの動作を理解するために、len()cap()を活用しましょう。

s := make([]int, 0, 10)
fmt.Printf("初期状態 - len: %d, cap: %d\n", len(s), cap(s))

s = append(s, 1, 2, 3)
fmt.Printf("3個追加後 - len: %d, cap: %d\n", len(s), cap(s))

このようにスライスの内部状態を可視化することで、予期しない動作の原因を特定できます。

まとめ

リンドくん

リンドくん

配列とスライスの違いがよくわかりました!基本的にはスライスを使っておけば間違いなさそうですね。

たなべ

たなべ

実際の開発では多くのケースでスライスを使うことになるよ。
ただし、配列の特性も理解しておくことで、より深いGo言語の理解につながるんだ。

本記事では、Go言語の配列とスライスの違いと使い分けについて詳しく解説してきました。

重要なポイントをおさらいしましょう。

  • 配列は固定サイズ、スライスは動的サイズという基本的な違い
  • パフォーマンス重視なら配列、柔軟性重視ならスライスという選択基準
  • 実際の開発ではスライスが主流だが、配列の特性も理解しておくことが重要
  • 参照共有やメモリ効率を意識した正しい使い方

現代のクラウドネイティブアプリケーション開発において、Go言語は重要な選択肢となっています。
配列とスライスの適切な使い分けができることで、メモリ効率的で保守性の高いコードを書けるようになります。

これからGo言語でのプログラミングスキルを向上させたい方、バックエンド開発に挑戦したい方は、ぜひ今回学んだ内容を実際のコードで試してみてください。
小さなプログラムから始めて、段階的にスライスの動的操作や配列の効率的な使用方法を身につけていきましょう。

この記事をシェア

関連するコンテンツ