Go: Goの配列とスライス:固定配列対動的スライス、追加によるサイズ変更の落とし穴、および基底となる配列の共有
スライスは、Goがコレクションデータを扱う仕組みの中核をなしています。スライスは配列の軽量な「ビュー」であり、その動的なサイズ変更機能により、コレクション操作は高速かつ柔軟なものとなっています。
Goでは、配列は(長さが固定されているため)直接使われることはほとんどありませんが、スライスは至る所で使われています。スライスを理解するには、まず配列を理解する必要があります。本質的に、スライスとは配列へのポインタに、その長さと容量を加えたものです。このレッスンでは、Goのコレクション型のすべての基本概念を習得します。
1. 学習内容
- 配列の定義、初期化、反復処理、および比較
- スライスを作成する4つの方法(リテラル/配列/make/null値)
len()とcap()の違いmakeの 3 つの引数の意味append自動サイズ調整機能- スライス
[low:high:max] - スライシングにおける基底配列の共有に伴う落とし穴(重要ポイント)
- スライスを用いた動的データコレクタの実装
2. データ収集エンジニアの実話
(1) 課題:固定サイズの配列では動的なデータを格納できない
アリスはデータ収集エンジニアです。彼女は最近、ウェブクローラーを作成する必要がありました:
「5つのデータソースからそれぞれ10,000件のレコードを取得し、重複を削除するためにメモリに格納する必要があります。最初はPythonのリストを使っていました。単純な『append』で済んでいました。Goに切り替えた後、
[10000]intのような固定長配列を使ってみましたが、最初のレコードで『index out of range』というエラーが出てパニックになりました。」
彼女は自分が書いた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での解決策:スライスの使用
// 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:])
}
出力:
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%のケース |
3. 配列
(1) 配列の定義と初期化
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)
}
出力:
[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) 配列の反復処理
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) 配列は値型である
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
}
出力:
Inside function: [999 2 3]
Outside function: [1 2 3]
▶ サンプル:配列の比較(同じ型かつ同じ長さの配列に限る)
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) スライス = 配列の「ビュー」
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 のスライスを直接宣言する
var s []int // nil slice, len=0, cap=0
// You can append, but you cannot access it using `s[0]`.
(3) 作成方法 2:リテラル
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 を指定)
// 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つの使い方の比較
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)
}
出力:
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 の関係
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 倍になります)。
▶ サンプル:len と cap の実際の違い
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)
}
}
出力:
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]
len == cap の場合、新しい上限は、古い上限 × 2(小さなスライスの場合)または古い上限 × 1.25(大きなスライスの場合、おおよそ 256 以上)となります。
6. 追加ベースの動的サイズ変更
(1) appendの基本
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)
}
出力:
[1 2 3 4 5 6 7]
▶ サンプル:append を使用した要素の削除(組み込み関数 delete なし)
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]
}
出力:
[1 2 4 5]
(3) appendの落とし穴:元の配列が変更される可能性がある
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]
}
出力:
s: [1 2 3 99 5]
s2: [1 2 3 99]
append を呼び出すと他のスライスに影響を与える可能性があります。厳密な分離が必要な場合は、copy() を使用するか、サイズ変更を強制してください。
7. Slice [low:high:max]
(1) 3種類のスライス式
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))
}
出力:
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 が元の配列の他の部分を上書きしないように制限を設定します:
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]
}
出力:
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]
8. スライシングにおける基底配列の共有に伴う落とし穴(重要ポイント)
(1) 落とし穴 1:スライス要素を変更すると、すべてのスライスに影響が及ぶ
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:append を for range と一緒に使用すると、無限ループが発生する
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:関数の引数として渡されたスライスが予期せず変更されてしまう
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() を使用して共有の問題を解決する
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
}
▶ サンプル:「コピー+追加」を使用してスライスを挿入する
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]
}
出力:
[1 2 3 4 5]
9. 完全な例:動的データコレクター
スライスのすべての機能を組み合わせ、複数ソースからのデータ収集+重複排除+フィルタリングを行うツールを構築します:
// 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()
}
期待される出力:
=== 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 による頻繁なサイズ変更を避けること。これは、パフォーマンスを最適化するための重要な手法です。
❓ よくある質問
append を実行すると、必ず配列のサイズが変更されますか?len == cap を実行した場合のみです。まだ空き容量がある場合は、要素は元の配列に直接書き込まれます。appendが外部のスライスに影響するかどうかは、スライスのサイズが変更されたかどうかによって異なります。[low:high:max] 形式の max パラメータは、どのような場合に必要ですか?append 操作が元の配列内の他の要素に影響を与えないように、スライスのサイズを制限したい場合に使用します。一般的な使用例:大きなスライスからサブスライスを作成し、その後 append 操作を行う場合です。list.remove(i)はありません。Goではappend(s[:i], s[i+1:]...)を使用して削除を実装しています(ただし、この方法では順序は保持されません)。順序を保持したい場合は、手動で要素の順序を並べ替える必要があります。copy(dst, src) — min(len(dst), len(src))個の要素をコピーします;(2) append([]T(nil), src...) — すべての要素をコピーします;(3) for range を手動で使用します。range を使用する場合、スライスと配列のパフォーマンスの違いは何ですか?range を使用すると、配列全体がコピーされるため(パフォーマンスが低い)、スライスに対して range を使用すると、24 バイトのみがコピーされるため(パフォーマンスが高い)です。これが、スライスが「コレクション」の事実上の標準となっている理由です。📖 まとめ
- 配列は値型(固定長、コピー渡し)であるのに対し、スライスは参照型(動的、基となるデータを共有して渡される)です。
- スライスを作成する4つの方法:nil / リテラル / make / 配列のスライス
lenは表示される要素の数、capは基となる配列の容量、appendはlen == cap(倍増戦略)のときに配列のサイズを変更する- 3つのパラメータを持つスライス式
[low:high:max]は、capに制限を設け、範囲外の追加を防ぐ - 基礎となる配列をスライスで共有すると、3つの典型的な落とし穴が生じます。それは、要素への変更が互いに影響し合うこと、
range内での追加、および関数による追加に伴う副作用です。 - 解決策:
copy()で別のコピーを作成する/サイズ変更を強制する (append(s, 0)[:0])/上限を設定する - 実際のプロジェクトでは、99%のケースでタイムスライスが使用されます。配列が使用されるのは、サイズが固定されている場合や値のセマンティクスが関わる場合に限られます。
📝 練習問題
-
基本問題(難易度 ⭐):スライスを使用して、スライスの要素を逆順にする
reverse([]int) []int関数を実装してください。ヒント:一時変数を使ったforループを使用するか、要素をその場で入れ替えてください。テストケース:reverse([]int{1,2,3,4,5})→[5 4 3 2 1]。 -
上級問題(難易度 ⭐⭐):
uniqueSorted(nums []int) []int関数を実装してください。重複を削除し、昇順にソートします。ヒント:まずsort.Intsを使用して配列をソートし、その後、配列を順に処理して重複を削除してください。入力として[]int{3, 1, 4, 1, 5, 9, 2, 6, 5}が与えられた場合、出力は[1 2 3 4 5 6 9]となるはずです。 -
チャレンジ問題(難易度 ⭐⭐⭐):スライディングウィンドウ最大値アルゴリズムを実装してください:
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))となる理由を説明すること。