Go: Goの配列とスライス:固定配列対動的スライス、追加によるサイズ変更の落とし穴、および基底となる配列の共有

スライスは、Goがコレクションデータを扱う仕組みの中核をなしています。スライスは配列の軽量な「ビュー」であり、その動的なサイズ変更機能により、コレクション操作は高速かつ柔軟なものとなっています。

Goでは、配列は(長さが固定されているため)直接使われることはほとんどありませんが、スライスは至る所で使われています。スライスを理解するには、まず配列を理解する必要があります。本質的に、スライスとは配列へのポインタに、その長さと容量を加えたものです。このレッスンでは、Goのコレクション型のすべての基本概念を習得します。

1. 学習内容



2. データ収集エンジニアの実話

(1) 課題:固定サイズの配列では動的なデータを格納できない

アリスはデータ収集エンジニアです。彼女は最近、ウェブクローラーを作成する必要がありました:

「5つのデータソースからそれぞれ10,000件のレコードを取得し、重複を削除するためにメモリに格納する必要があります。最初はPythonのリストを使っていました。単純な『append』で済んでいました。Goに切り替えた後、[10000]intのような固定長配列を使ってみましたが、最初のレコードで『index out of range』というエラーが出てパニックになりました。」

彼女は自分が書いたGoのコードを開いた:

GO
// Version 1: Fixed-length array; cannot grow
var records [10000]int
for i := 0; i < 50000; i++ {
    records[i] = i  // ❌ i >= 10000 causes panic!
}

上司は「データソースの数は日々変わるから、固定長の配列ではうまくいかないよ」と言った。そのとき初めて、アリスはGoの配列が値型であり、長さが型の一部であることを理解した――[5]int[10]intは異なる型なのだ。

(2) Goでの解決策:スライスの使用

GO
// data_collector.go
package main

import "fmt"

func main() {
    // Slices: Dynamic length, automatic resizing
    var records []int  // nil slice, can append

    // Simulate retrieving 10,000 records from each of 5 data sources
    for source := 1; source <= 5; source++ {
        for i := 0; i < 10000; i++ {
            value := source*10000 + i
            records = append(records, value)  // auto-resize
        }
    }

    fmt.Printf("Total number of records: %d\n", len(records))
    fmt.Printf("Underlying capacity: %d\n", cap(records))
    fmt.Printf("Top 3: %v\n", records[:3])
    fmt.Printf("Last 3: %v\n", records[len(records)-3:])
}

出力:

TEXT 📖 参照専用
Total number of records: 50000
Underlying capacity: 65536
Top 3: [10000 10001 10002]
Last 3: [59997 59998 59999]

(3) パフォーマンス:配列とスライスの比較

次元 配列 [n]T スライス []T
長さ コンパイル時に固定 実行時に可変
タイプ [5]int[10]int []int 汎用
パラメータの渡し方 値渡し(配列全体をコピーする) 参照渡し(24バイトのみ)
サイズ変更 未対応 append 自動
ユースケース 固定サイズ(例:バッファ) 99%のケース
💡 ヒント: Go言語では、「コレクション」として扱われるケースの99%でスライスが使用されます。配列が使用されるのは、サイズが固定されている場合(SHA-256用の32バイトのバッファなど)や、値セマンティクスが必要な場合に限られます。



3. 配列

(1) 配列の定義と初期化

GO
package main

import "fmt"

func main() {
    // Method 1: Declaration + Initialization
    var a1 [5]int = [5]int{1, 2, 3, 4, 5}

    // Method 2: Type Inference
    a2 := [5]int{1, 2, 3, 4, 5}

    // Method 3: Partial Initialization (with the Rest Set to Zero)
    a3 := [5]int{1, 2}  // [1 2 0 0 0]

    // Method 4: Use ... to let the compiler infer the length
    a4 := [...]int{1, 2, 3, 4}  // length 4

    // Method 5: Specifying Index Initialization
    a5 := [5]int{0: 10, 4: 50}  // [10 0 0 0 50]

    fmt.Println(a1, a2, a3, a4, a5)
}

出力:

TEXT 📖 参照専用
[1 2 3 4 5] [1 2 3 4 5] [1 2 0 0 0] [1 2 3 4] [10 0 0 0 50]

(2) 配列の反復処理

GO
package main

import "fmt"

func main() {
    nums := [4]string{"Alice", "Bob", "Charlie", "Dave"}

    // Method 1: for loop
    for i := 0; i < len(nums); i++ {
        fmt.Printf("%d: %s\n", i, nums[i])
    }

    // Method 2: for range
    for i, name := range nums {
        fmt.Printf("[%d] %s\n", i, name)
    }
}

(3) 配列は値型である

GO
package main

import "fmt"

func modify(arr [3]int) {
    arr[0] = 999  // modifies the copy
    fmt.Printf("Inside function: %v\n", arr)
}

func main() {
    a := [3]int{1, 2, 3}
    modify(a)
    fmt.Printf("Outside function: %v\n", a)  // original array unchanged
}

出力:

TEXT 📖 参照専用
Inside function: [999 2 3]
Outside function: [1 2 3]
🔥 よくある間違い: Goの配列は値型であるため、引数として渡すと配列全体がコピーされます。100万個の要素を持つ配列を引数として渡すことは、100万個の要素をコピーすることになり、パフォーマンス上の大惨事となります。代わりにスライスを使用してください。

▶ サンプル:配列の比較(同じ型かつ同じ長さの配列に限る)

GO
package main

import "fmt"

func main() {
    a := [3]int{1, 2, 3}
    b := [3]int{1, 2, 3}
    c := [3]int{1, 2, 4}

    fmt.Println(a == b)  // true
    fmt.Println(a == c)  // false

    // Items of different lengths or types cannot be compared.
    // d := [4]int{1, 2, 3, 4}
    // fmt.Println(a == d)  // ❌ mismatched types
}
▶ 試してみよう

4. スライスを作成する4つの方法

(1) スライス = 配列の「ビュー」

100%
graph TB
    subgraph Slice["Slice (24 bytes)"]
        P["ptr<br/>points to the underlying array"]
        L["len<br/>length"]
        C["cap<br/>capacity"]
    end
    subgraph Array["Underlying array"]
        A0["[0]"]
        A1["[1]"]
        A2["[2]"]
        A3["[3]"]
        A4["[4]"]
        A5["[5]"]
    end
    P --> A1
    P -.-> A2
    P -.-> A3

スライスは基となる配列への参照であり、そのサイズはわずか24バイト(ptr + len + cap)です。複数のスライスが同じ基となる配列を共有することができます。

(2) 作成方法 1:nil のスライスを直接宣言する

GO
var s []int  // nil slice, len=0, cap=0
// You can append, but you cannot access it using `s[0]`.

(3) 作成方法 2:リテラル

GO
s := []int{1, 2, 3, 4, 5}  // create and initialize
// Base array = [1 2 3 4 5], len = cap = 5

(4) 作成方法 3: make(len と cap を指定)

GO
// Syntax: make([]T, len, cap)
s := make([]int, 5)       // len=5, cap=5, all zero values [0 0 0 0 0]
s := make([]int, 3, 10)   // len=3, cap=10

▶ サンプル:「make」の4つの使い方の比較

GO
package main

import "fmt"

func main() {
    // Method 1: nil slice
    var s1 []int
    fmt.Printf("s1: len=%d cap=%d nil=%v\n", len(s1), cap(s1), s1 == nil)

    // Method 2: Literal
    s2 := []int{1, 2, 3}
    fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)

    // Method 3: Set the length
    s3 := make([]int, 5)
    fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)

    // Method 4: make (length + capacity)
    s4 := make([]int, 3, 10)
    fmt.Printf("s4: len=%d cap=%d %v\n", len(s4), cap(s4), s4)

    // Method 5: Slicing from an array
    arr := [5]int{10, 20, 30, 40, 50}
    s5 := arr[1:4]  // [20 30 40], len=3 cap=4 (4 remaining elements)
    fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
s1: len=0 cap=0 nil=true
s2: len=3 cap=3 [1 2 3]
s3: len=5 cap=5 [0 0 0 0 0]
s4: len=3 cap=10 [0 0 0]
s5: len=3 cap=4 [20 30 40]

(6) クイックリファレンス:作成する4つの方法

メソッド 構文 使用例
nil スライス var s []T 遅延初期化
リテラル []T{1, 2, 3} すべての要素が既知
make make([]T, len, cap) 容量の事前割り当て
配列のスライシング arr[low:high] サブ配列のビュー


5. len() と cap()

(1) len と cap の関係

GO
package main

import "fmt"

func main() {
    s := make([]int, 3, 10)  // len=3, cap=10

    fmt.Printf("Initial: len=%d cap=%d\n", len(s), cap(s))
    // len=3: You can access s[0], s[1], and s[2]
    // cap=10: The underlying array has 10 elements, with room for 7 more to be appended
}

(2) 「キャップ」の意味

cap - len = サイズを変更せずに追加できる要素の数。これが cap を超える場合、append は新しい基底配列を割り当てます(通常、サイズは 2 倍になります)。

▶ サンプル:lencap の実際の違い

GO
package main

import "fmt"

func main() {
    s := make([]int, 0, 5)  // len=0, cap=5

    for i := 1; i <= 8; i++ {
        s = append(s, i)
        fmt.Printf("append %d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
    }
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
append 1: len=1 cap=5 [1]
append 2: len=2 cap=5 [1 2]
append 3: len=3 cap=5 [1 2 3]
append 4: len=4 cap=5 [1 2 3 4]
append 5: len=5 cap=5 [1 2 3 4 5]
append 6: len=6 cap=10 [1 2 3 4 5 6]  ← cap doubled
append 7: len=7 cap=10 [1 2 3 4 5 6 7]
append 8: len=8 cap=10 [1 2 3 4 5 6 7 8]
💡 ヒント: Goのサイズ変更戦略:len == cap の場合、新しい上限は、古い上限 × 2(小さなスライスの場合)または古い上限 × 1.25(大きなスライスの場合、おおよそ 256 以上)となります。



6. 追加ベースの動的サイズ変更

(1) appendの基本

GO
package main

import "fmt"

func main() {
    var s []int  // nil

    s = append(s, 1)       // [1]
    s = append(s, 2, 3, 4) // [1 2 3 4]

    // Batch append: Splitting a slice
    s2 := []int{5, 6, 7}
    s = append(s, s2...)   // [1 2 3 4 5 6 7]

    fmt.Println(s)
}

出力:

TEXT 📖 参照専用
[1 2 3 4 5 6 7]

▶ サンプル:append を使用した要素の削除(組み込み関数 delete なし)

GO
package main

import "fmt"

// Delete the element at index i
func removeAt(s []int, i int) []int {
    // Concatenation: s[:i] + s[i+1:]
    return append(s[:i], s[i+1:]...)
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = removeAt(s, 2)  // delete 3
    fmt.Println(s)       // [1 2 4 5]
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
[1 2 4 5]

(3) appendの落とし穴:元の配列が変更される可能性がある

GO
package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    s2 := s[:3]  // [1 2 3], shares underlying array

    s2 = append(s2, 99)  // no resize, modified s's underlying array!
    fmt.Println("s:", s)    // [1 2 3 99 5]
    fmt.Println("s2:", s2)  // [1 2 3 99]
}

出力:

TEXT 📖 参照専用
s: [1 2 3 99 5]
s2: [1 2 3 99]
🔥 よくある間違い: これはGo言語における最も典型的な落とし穴の一つです。スライスは基となる配列を共有しているため、append を呼び出すと他のスライスに影響を与える可能性があります。厳密な分離が必要な場合は、copy() を使用するか、サイズ変更を強制してください。



7. Slice [low:high:max]

(1) 3種類のスライス式

GO
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}

    // Form 1: [low:high] — Takes [low, high)
    s1 := arr[1:4]
    fmt.Printf("arr[1:4] = %v, len=%d cap=%d\n", s1, len(s1), cap(s1))

    // Format 2: [low:] — From low to the end
    s2 := arr[2:]
    fmt.Printf("arr[2:]  = %v, len=%d cap=%d\n", s2, len(s2), cap(s2))

    // Form 3: [:high] — From the beginning to high
    s3 := arr[:3]
    fmt.Printf("arr[:3]  = %v, len=%d cap=%d\n", s3, len(s3), cap(s3))

    // Format 4: [low:high:max] — Sets a cap (to prevent unexpected additions from affecting the original array)
    s4 := arr[1:3:4]
    fmt.Printf("arr[1:3:4] = %v, len=%d cap=%d\n", s4, len(s4), cap(s4))
}

出力:

TEXT 📖 参照専用
arr[1:4] = [20 30 40], len=3 cap=4
arr[2:]  = [30 40 50], len=3 cap=3
arr[:3]  = [10 20 30], len=3 cap=5
arr[1:3:4] = [20 30], len=2 cap=3

(2) [low:high:max]の目的

max は、append が元の配列の他の部分を上書きしないように制限を設定します:

GO
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}

    // No limit on max: cap=4; after an append operation, arr[3] will be overwritten
    s1 := arr[1:3]  // [20 30], cap=4
    s1 = append(s1, 999)
    fmt.Println("arr:", arr)  // [10 20 30 999 50] ← overwritten!

    // Limit max=3: cap=2; append creates a new array
    s2 := arr[1:3:3]  // [20 30], cap=2
    s2 = append(s2, 888)
    fmt.Println("arr:", arr)  // [10 20 30 999 50] ← unchanged
    fmt.Println("s2:", s2)    // [20 30 888]
}

出力:

TEXT 📖 参照専用
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]


8. スライシングにおける基底配列の共有に伴う落とし穴(重要ポイント)

(1) 落とし穴 1:スライス要素を変更すると、すべてのスライスに影響が及ぶ

GO
package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s1 := arr[0:3]  // [1 2 3]
    s2 := arr[2:5]  // [3 4 5]

    s1[2] = 999  // same underlying array!
    fmt.Println("s1:", s1)  // [1 2 999]
    fmt.Println("s2:", s2)  // [999 4 5]
    fmt.Println("arr:", arr) // [1 2 999 4 5]
}

(2) 落とし穴 2:appendfor range と一緒に使用すると、無限ループが発生する

GO
package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    for _, v := range s {
        s = append(s, v*10)  // dangerous: s is growing
    }
    fmt.Println(s)  // output is undefined
}
🔥 よくある間違い: for range ループ中は、スライス自体を絶対に変更してはいけません。Go 1.22 以前では、for i, v := range s 内の s はループ開始時点での長さのスナップショットでした。Go 1.22 では一部のシナリオにおいてこの問題が修正されましたが、依然として注意が必要です。

(3) 落とし穴 3:関数の引数として渡されたスライスが予期せず変更されてしまう

GO
package main

import "fmt"

// Slicing uses pass-by-reference, so changes made within the function affect the outside.
func addElement(s []int) {
    s = append(s, 100)  // may modify the original array (if no resize)
    fmt.Printf("Inside function: %v\n", s)
}

func main() {
    s := make([]int, 3, 10)
    s[0], s[1], s[2] = 1, 2, 3

    addElement(s)
    fmt.Printf("Outside function: %v\n", s)  // depends on whether resize occurred
}

(4) copy() を使用して共有の問題を解決する

GO
package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s1 := arr[0:3]

    // Create a separate copy
    s2 := make([]int, len(s1))
    copy(s2, s1)

    s1[0] = 999
    fmt.Println("s1:", s1)  // [999 2 3]
    fmt.Println("s2:", s2)  // [1 2 3] ← unaffected
}

▶ サンプル:「コピー+追加」を使用してスライスを挿入する

GO
package main

import "fmt"

// Insert element val at index i
func insert(s []int, i int, val int) []int {
    // 1. Add 1 position
    s = append(s, 0)

    // 2. Shift the element at index i and all subsequent elements one position to the right
    copy(s[i+1:], s[i:])

    // 3. Add a new element
    s[i] = val

    return s
}

func main() {
    s := []int{1, 2, 4, 5}
    s = insert(s, 2, 3)
    fmt.Println(s)  // [1 2 3 4 5]
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
[1 2 3 4 5]


9. 完全な例:動的データコレクター

スライスのすべての機能を組み合わせ、複数ソースからのデータ収集+重複排除+フィルタリングを行うツールを構築します:

GO
// collector.go
package main

import (
    "fmt"
    "strings"
)

// Data Source Simulation Functions
func fetchFromSource(sourceName string) []string {
    data := map[string][]string{
        "GitHub": {"repo:go", "repo:docker", "issue:bug", "repo:go"},  // last one is duplicate
        "NPM":    {"pkg:react", "pkg:vue", "pkg:react"},              // duplicate
        "PyPI":   {"pkg:requests", "pkg:flask"},
    }
    return data[sourceName]
}

// Removing Duplicates: Using `map` to Create a Hash Set
func deduplicate(data []string) []string {
    seen := make(map[string]bool)
    result := make([]string, 0, len(data))

    for _, item := range data {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

// Filter: Keep only those starting with "repo:"
func filterRepos(data []string) []string {
    result := make([]string, 0)
    for _, item := range data {
        if strings.HasPrefix(item, "repo:") {
            result = append(result, item)
        }
    }
    return result
}

// Pipeline: Combining Multiple Steps
func collectPipeline() (total, unique, repos int, sources []string) {
    defer func() {
        // Naming Return Values + Defer Collection Statistics
        fmt.Printf("\n[Collection Complete] total=%d unique=%d repos=%d sources=%v\n",
            total, unique, repos, sources)
    }()

    allSources := []string{"GitHub", "NPM", "PyPI"}
    var allData []string

    for _, src := range allSources {
        sources = append(sources, src)
        data := fetchFromSource(src)
        allData = append(allData, data...)
        total += len(data)
    }

    uniqueData := deduplicate(allData)
    unique = len(uniqueData)

    repoData := filterRepos(uniqueData)
    repos = len(repoData)

    fmt.Println("\n=== Deduped Data ===")
    for _, item := range uniqueData {
        fmt.Printf("  %s\n", item)
    }

    fmt.Println("\n=== Filtering Repo Data ===")
    for _, item := range repoData {
        fmt.Printf("  %s\n", item)
    }

    return total, unique, repos, sources
}

func main() {
    collectPipeline()
}

期待される出力:

TEXT 📖 参照専用
=== Deduped Data ===
  repo:go
  repo:docker
  issue:bug
  pkg:react
  pkg:vue
  pkg:requests
  pkg:flask

=== Filtering Repo Data ===
  repo:go
  repo:docker

[Collection Complete] total=9 unique=7 repos=2 sources=[GitHub NPM PyPI]
🔥 よくある間違い: make([]string, 0, len(data)) を使用して容量を事前に割り当て、append による頻繁なサイズ変更を避けること。これは、パフォーマンスを最適化するための重要な手法です。


❓ よくある質問

Q 配列とスライスの根本的な関係は何ですか?
A スライスは、本質的に配列の一つのビューです。スライスは24バイトで構成されます。具体的には、8バイトのポインタ(配列内の要素を指す)、8バイトの長さ、8バイトの容量です。複数のスライスが、同じ基となる配列を共有することができます。
Q append を実行すると、必ず配列のサイズが変更されますか?
A 必ずしもそうとは限りません。配列のサイズが変更される(新しい配列を割り当て、内容を2回コピーする)のは、len == cap を実行した場合のみです。まだ空き容量がある場合は、要素は元の配列に直接書き込まれます。
Q スライスは関数に値渡しで渡されるのか、参照渡しで渡されるのか?
A スライス自体(24バイト)は値渡しで渡されますが、それが指す基となる配列は共有されています。したがって、関数内で要素を変更すると外部のスライスに影響しますが、appendが外部のスライスに影響するかどうかは、スライスのサイズが変更されたかどうかによって異なります。
Q [low:high:max] 形式の max パラメータは、どのような場合に必要ですか?
A append 操作が元の配列内の他の要素に影響を与えないように、スライスのサイズを制限したい場合に使用します。一般的な使用例:大きなスライスからサブスライスを作成し、その後 append 操作を行う場合です。
Q スライスから要素を削除するための組み込みメソッドはありますか?
A いいえ。Javaとは異なり、Goにはlist.remove(i)はありません。Goではappend(s[:i], s[i+1:]...)を使用して削除を実装しています(ただし、この方法では順序は保持されません)。順序を保持したい場合は、手動で要素の順序を並べ替える必要があります。
Q スライスをコピーするにはどうすればよいですか?
A 3つの方法があります:(1) copy(dst, src) — min(len(dst), len(src))個の要素をコピーします;(2) append([]T(nil), src...) — すべての要素をコピーします;(3) for range を手動で使用します。
Q range を使用する場合、スライスと配列のパフォーマンスの違いは何ですか?
A 配列に対して range を使用すると、配列全体がコピーされるため(パフォーマンスが低い)、スライスに対して range を使用すると、24 バイトのみがコピーされるため(パフォーマンスが高い)です。これが、スライスが「コレクション」の事実上の標準となっている理由です。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):スライスを使用して、スライスの要素を逆順にする reverse([]int) []int 関数を実装してください。ヒント:一時変数を使った for ループを使用するか、要素をその場で入れ替えてください。テストケース:reverse([]int{1,2,3,4,5})[5 4 3 2 1]

  2. 上級問題(難易度 ⭐⭐)uniqueSorted(nums []int) []int関数を実装してください。重複を削除し、昇順にソートします。ヒント:まずsort.Intsを使用して配列をソートし、その後、配列を順に処理して重複を削除してください。入力として[]int{3, 1, 4, 1, 5, 9, 2, 6, 5}が与えられた場合、出力は[1 2 3 4 5 6 9]となるはずです。

  3. チャレンジ問題(難易度 ⭐⭐⭐)スライディングウィンドウ最大値アルゴリズムを実装してください:maxSlidingWindow(nums []int, k int) []int。例:nums=[1,3,-1,-3,5,3,6,7], k=3 → 出力 [3,3,5,5,6,7]。要件:(1) 時間計算量 O(n) であること;(2) ウィンドウを維持するために deque[]int を用いてシミュレート可能)を使用すること;(3) 直接走査を行うとタイムアウト(O(n*k))となる理由を説明すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%