Day 6: 配列とスライス - 背景知識

目次

Goがスライスを導入した背景

Go言語の開発者(Robert Griesemer、Rob Pike、Ken Thompson)は、Google内部での大規模システム開発における課題を解決するためにGoを設計しました。

C/C++の課題:

  • 配列のサイズ情報がポインタに含まれない
  • バッファオーバーランのセキュリティリスク
  • 動的配列(std::vector)の複雑な実装

Java/Pythonの課題:

  • ガベージコレクションのオーバーヘッド
  • 実行速度の問題
  • メモリ効率の低下

Goのソリューション: スライスという軽量で安全な動的配列の抽象化を言語レベルで提供。

// C言語の配列(サイズ情報なし)
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]); // 手動計算

// Goのスライス(サイズ情報組み込み)
slice := []int{1, 2, 3, 4, 5}
size := len(slice) // 自動的に取得可能

---

Goにおける配列とスライスの設計思想

配列:型の一部としてのサイズ

Goの配列は、サイズが型の一部として扱われる点で他の言語と異なります。

var a [5]int  // [5]int型
var b [10]int // [10]int型(異なる型!)

// 以下はコンパイルエラー
// a = b  // cannot use b (type [10]int) as type [5]int

設計意図:

  • コンパイル時の型安全性
  • スタック割り当てによる高速アクセス
  • メモリレイアウトの予測可能性

スライス:参照型としての柔軟性

スライスは内部的に以下の3つの要素で構成されます:

type slice struct {
    array unsafe.Pointer  // 基底配列へのポインタ
    len   int              // 現在の長さ
    cap   int              // 容量
}

ビジュアル表現:

スライス変数
┌─────────────┐
│ ptr     ────┼──> [10][20][30][40][50][ ][ ][ ]
│ len: 5      │     └────────┘└─────────────┘
│ cap: 8      │       len=5        cap=8
└─────────────┘

---

実世界での活用事例

1. Google(検索エンジン)

使用場面: 検索結果のランキングとページネーション

// Google検索結果の内部処理(簡略化)
type SearchResult struct {
    Title       string
    URL         string
    Snippet     string
    Relevance   float64
}

// 数百万件の検索結果をスライスで管理
func ProcessSearchResults(query string) []SearchResult {
    results := make([]SearchResult, 0, 1000000)

    // 検索処理
    for _, doc := range searchIndex {
        if matches(doc, query) {
            results = append(results, calculateRelevance(doc, query))
        }
    }

    // ランキング(スライスのソート)
    sort.Slice(results, func(i, j int) bool {
        return results[i].Relevance > results[j].Relevance
    })

    return results
}

// ページネーション(スライシング)
func GetPage(results []SearchResult, page, pageSize int) []SearchResult {
    start := page * pageSize
    end := start + pageSize

    if start >= len(results) {
        return []SearchResult{}
    }
    if end > len(results) {
        end = len(results)
    }

    return results[start:end]  // O(1)の操作
}

効果:

  • メモリ効率的な大規模データ処理
  • 高速なページング処理(O(1)のスライス操作)
  • 並行処理との親和性

2. Docker(コンテナプラットフォーム)

使用場面: コンテナイメージのレイヤー管理

// Dockerイメージレイヤーの管理
type Layer struct {
    ID       string
    Size     int64
    Checksum string
    Parent   *Layer
}

type Image struct {
    Layers []Layer  // スライスで複数レイヤーを管理
}

// レイヤーの追加(イメージのビルド)
func (img *Image) AddLayer(layer Layer) {
    img.Layers = append(img.Layers, layer)
}

// レイヤーの共有(Copy-on-Write)
func ShareLayers(base Image, derived Image) []Layer {
    // スライシングで共通レイヤーを識別
    commonLayers := findCommonPrefix(base.Layers, derived.Layers)
    return commonLayers
}

効果:

  • レイヤーの効率的な管理
  • Copy-on-Writeの実装
  • ストレージ最適化

3. Uber(配車プラットフォーム)

使用場面: リアルタイムドライバーマッチング

// ドライバーの位置情報管理
type Driver struct {
    ID        string
    Lat       float64
    Lng       float64
    Available bool
}

// 周辺ドライバーの検索
func FindNearbyDrivers(userLat, userLng float64, radius float64) []Driver {
    nearbyDrivers := make([]Driver, 0, 100)

    // 全ドライバーから検索
    for _, driver := range allDrivers {
        if driver.Available && isWithinRadius(driver, userLat, userLng, radius) {
            nearbyDrivers = append(nearbyDrivers, driver)
        }
    }

    // 距離でソート
    sort.Slice(nearbyDrivers, func(i, j int) bool {
        distI := calculateDistance(nearbyDrivers[i], userLat, userLng)
        distJ := calculateDistance(nearbyDrivers[j], userLat, userLng)
        return distI < distJ
    })

    return nearbyDrivers
}

効果:

  • リアルタイムデータ処理
  • 動的なドライバープールの管理
  • 高速な検索とソート

4. Netflix(ストリーミングサービス)

使用場面: レコメンデーションシステム

// 視聴履歴とレコメンデーション
type Video struct {
    ID       string
    Title    string
    Genre    []string  // スライスで複数ジャンル
    Rating   float64
}

type User struct {
    ID      string
    History []Video   // 視聴履歴
}

// パーソナライズドレコメンデーション
func GenerateRecommendations(user User) []Video {
    // ジャンル分析(スライス操作)
    genreCount := make(map[string]int)
    for _, video := range user.History {
        for _, genre := range video.Genre {
            genreCount[genre]++
        }
    }

    // トップジャンルの抽出
    topGenres := getTopN(genreCount, 3)

    // レコメンデーション生成
    recommendations := make([]Video, 0, 50)
    for _, video := range catalogVideos {
        if hasAnyGenre(video, topGenres) && !watched(user, video) {
            recommendations = append(recommendations, video)
        }
    }

    return recommendations
}

効果:

  • 複雑なデータパターンの処理
  • 高速なフィルタリング
  • スケーラブルなレコメンデーション

5. Cloudflare(CDN・セキュリティ)

使用場面: DDoS攻撃の検出と防御

// リクエストレート制限
type RequestLog struct {
    IP        string
    Timestamp time.Time
    Path      string
}

// スライディングウィンドウでレート制限
func CheckRateLimit(ip string, logs []RequestLog, windowSec int) bool {
    cutoff := time.Now().Add(-time.Duration(windowSec) * time.Second)

    // 時間内のリクエストをフィルタリング
    recentRequests := make([]RequestLog, 0)
    for _, log := range logs {
        if log.IP == ip && log.Timestamp.After(cutoff) {
            recentRequests = append(recentRequests, log)
        }
    }

    // しきい値チェック
    return len(recentRequests) > 100
}

効果:

  • 高速なセキュリティ分析
  • リアルタイムトラフィック監視
  • メモリ効率的なログ処理

6. その他の企業事例

Dropbox: ファイル同期のチャンク管理

type FileChunk struct {
    Offset int64
    Data   []byte
    Hash   string
}

func SyncFile(chunks []FileChunk) error {
    for _, chunk := range chunks {
        if err := uploadChunk(chunk); err != nil {
            return err
        }
    }
    return nil
}

Spotify: プレイリストの動的管理

type Track struct {
    ID       string
    Title    string
    Duration int
}

type Playlist struct {
    Tracks []Track
}

func (p *Playlist) Shuffle() {
    rand.Shuffle(len(p.Tracks), func(i, j int) {
        p.Tracks[i], p.Tracks[j] = p.Tracks[j], p.Tracks[i]
    })
}

---

市場価値分析

Go開発者の年収トレンド(2025年)

地域別平均年収:

地域 ジュニア ミドル シニア
東京 450万円 700万円 1000万円+
大阪 400万円 650万円 900万円+
福岡 380万円 600万円 850万円+
リモート 500万円 750万円 1200万円+

グローバル市場:

地域 平均年収(USD)
シリコンバレー $150,000 - $250,000
ニューヨーク $130,000 - $220,000
ロンドン £70,000 - £120,000
シンガポール SGD 80,000 - SGD 150,000

求人市場の動向

2024-2025年のGo求人トレンド:

  • クラウドネイティブ開発: 40%増
  • マイクロサービス: 35%増
  • DevOps/SREエンジニア: 45%増
  • データエンジニアリング: 30%増

求められるスキルセット:

基礎スキル(必須):
├── 配列・スライスの深い理解 ★
├── マップ、構造体
├── 並行処理(goroutine, channel)
└── エラーハンドリング

実務スキル(重要):
├── Docker/Kubernetes
├── gRPC/REST API
├── データベース(PostgreSQL, Redis)
└── クラウド(AWS, GCP, Azure)

ソフトスキル(差別化):
├── コードレビュー
├── チーム協働
├── ドキュメンテーション
└── 問題解決能力

キャリア成長機会

配列・スライスの習得がもたらすキャリアパス:

レベル1: ジュニア開発者(0-2年)
└─ 配列・スライスの基本操作
   └─ CRUD操作の実装

レベル2: ミドル開発者(2-5年)
└─ パフォーマンス最適化
   ├─ メモリ効率の意識
   ├─ アルゴリズムの選択
   └─ ベンチマーク測定

レベル3: シニア開発者(5-10年)
└─ アーキテクチャ設計
   ├─ スケーラビリティ考慮
   ├─ データ構造の設計
   └─ コードレビュー・メンタリング

レベル4: リードエンジニア/アーキテクト(10年+)
└─ システム全体設計
   ├─ 技術選定
   ├─ チームリーダーシップ
   └─ ビジネス価値創出

---

基本概念と文法

配列(Array)

配列は固定長のデータ構造です。

宣言と初期化

// 方法1: サイズ指定
var arr [5]int               // [0, 0, 0, 0, 0]

// 方法2: リテラル
arr := [5]int{1, 2, 3, 4, 5}

// 方法3: 長さ自動推論
arr := [...]int{1, 2, 3}     // [3]int型

// 方法4: インデックス指定初期化
arr := [5]int{0: 10, 2: 30, 4: 50}  // [10, 0, 30, 0, 50]

多次元配列

// 2次元配列
var matrix [3][3]int

// 初期化
matrix := [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

// アクセス
fmt.Println(matrix[1][1])  // 5

配列の特性

// 値渡し(コピー)
func modifyArray(arr [5]int) {
    arr[0] = 100  // コピーを変更
}

arr := [5]int{1, 2, 3, 4, 5}
modifyArray(arr)
fmt.Println(arr[0])  // 1(元の値は変わらない)

---

スライス(Slice)

スライスは可変長のデータ構造です。

宣言と初期化

// 方法1: リテラル
slice := []int{1, 2, 3}

// 方法2: make関数
slice := make([]int, 5)      // 長さ5、容量5
slice := make([]int, 5, 10)  // 長さ5、容量10

// 方法3: nil スライス
var slice []int              // nil(長さ0、容量0)

// 方法4: 空スライス
slice := []int{}             // 非nil(長さ0、容量0)

長さと容量

slice := make([]int, 5, 10)

fmt.Println(len(slice))  // 5(長さ)
fmt.Println(cap(slice))  // 10(容量)

// 容量を超えるappend
slice = append(slice, 1, 2, 3, 4, 5, 6)
fmt.Println(len(slice))  // 11
fmt.Println(cap(slice))  // 20(自動的に拡張)

容量拡張のメカニズム:

初期容量: 10
├─ 容量不足時: 新しい配列を確保(通常は2倍)
├─ 古いデータをコピー
└─ スライスのポインタを更新

容量拡張のコスト: O(n)

スライシング

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

// 基本形: slice[start:end](endは含まない)
fmt.Println(slice[2:5])   // [2 3 4]
fmt.Println(slice[:3])    // [0 1 2](最初から)
fmt.Println(slice[7:])    // [7 8 9](最後まで)
fmt.Println(slice[:])     // 全体のコピー

// 3番目のインデックス: 容量指定
s2 := slice[2:5:7]
// 長さ: 5-2=3
// 容量: 7-2=5

append操作

// 単一要素の追加
slice := []int{1, 2, 3}
slice = append(slice, 4)

// 複数要素の追加
slice = append(slice, 5, 6, 7)

// スライスの結合
slice2 := []int{8, 9, 10}
slice = append(slice, slice2...)  // ...で展開

// 注意: appendは新しいスライスを返す
func appendDemo() {
    s1 := []int{1, 2, 3}
    s2 := s1
    s1 = append(s1, 4)

    fmt.Println(s1)  // [1 2 3 4]
    fmt.Println(s2)  // [1 2 3](影響なし)
}

---

for rangeループ

基本構文

nums := []int{10, 20, 30}

// インデックスと値の両方
for i, v := range nums {
    fmt.Printf("nums[%d] = %d\n", i, v)
}

// 値のみ
for _, v := range nums {
    fmt.Println(v)
}

// インデックスのみ
for i := range nums {
    fmt.Println(i)
}

// レシーバなし(単なる回数ループ)
for range nums {
    fmt.Println("処理")
}

rangeの注意点

// 注意1: 値はコピー
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {Name: "太郎", Age: 25},
    {Name: "花子", Age: 22},
}

// NG: 値を変更しても元は変わらない
for _, p := range people {
    p.Age++  // コピーを変更
}
fmt.Println(people[0].Age)  // 25(変わらない)

// OK: インデックスでアクセス
for i := range people {
    people[i].Age++
}
fmt.Println(people[0].Age)  // 26

// 注意2: rangeの評価タイミング
slice := []int{1, 2, 3}
for i, v := range slice {
    slice = append(slice, v)  // 無限ループにならない
    fmt.Println(i, v)
}
// 出力: 0 1, 1 2, 2 3(3回のみ)
// 理由: rangeはループ開始時にスライスのコピーを評価

---

モダン開発プラクティス

1. テスト駆動開発(TDD)

配列・スライス操作のTDD例:

// sum_test.go
package main

import "testing"

func TestSum(t *testing.T) {
    tests := []struct {
        name     string
        input    []int
        expected int
    }{
        {"空スライス", []int{}, 0},
        {"単一要素", []int{5}, 5},
        {"複数要素", []int{1, 2, 3, 4, 5}, 15},
        {"負の値", []int{-1, -2, -3}, -6},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Sum(tt.input)
            if result != tt.expected {
                t.Errorf("Sum(%v) = %d; want %d", tt.input, result, tt.expected)
            }
        })
    }
}

// sum.go
func Sum(numbers []int) int {
    sum := 0
    for _, n := range numbers {
        sum += n
    }
    return sum
}

2. ベンチマーク測定

// benchmark_test.go
func BenchmarkAppend(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := make([]int, 0)
        for j := 0; j < 1000; j++ {
            slice = append(slice, j)
        }
    }
}

func BenchmarkAppendWithCapacity(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := make([]int, 0, 1000)  // 容量を事前確保
        for j := 0; j < 1000; j++ {
            slice = append(slice, j)
        }
    }
}

// 実行結果:
// BenchmarkAppend-8                  50000    35000 ns/op
// BenchmarkAppendWithCapacity-8     100000    15000 ns/op
// → 容量事前確保で2倍以上高速化

3. コードレビューのポイント

// BAD: 容量を考慮していない
func ProcessData(data []int) []int {
    result := []int{}  // 容量0から開始
    for _, v := range data {
        result = append(result, v*2)  // 何度も再割り当て
    }
    return result
}

// GOOD: 容量を事前確保
func ProcessData(data []int) []int {
    result := make([]int, 0, len(data))  // 必要な容量を確保
    for _, v := range data {
        result = append(result, v*2)
    }
    return result
}

// BETTER: インデックスアクセスで更に高速化
func ProcessData(data []int) []int {
    result := make([]int, len(data))  // 長さも指定
    for i, v := range data {
        result[i] = v * 2  // appendではなく直接代入
    }
    return result
}

4. デバッグ戦略

// デバッグ用のヘルパー関数
func debugSlice(name string, s []int) {
    fmt.Printf("%s: len=%d, cap=%d, data=%v\n", name, len(s), cap(s), s)
}

func main() {
    s := make([]int, 0, 5)
    debugSlice("初期", s)  // 初期: len=0, cap=5, data=[]

    s = append(s, 1, 2, 3)
    debugSlice("追加後", s)  // 追加後: len=3, cap=5, data=[1 2 3]

    s = append(s, 4, 5, 6)
    debugSlice("容量超過", s)  // 容量超過: len=6, cap=10, data=[1 2 3 4 5 6]
}

5. CI/CDパイプライン統合

# .github/workflows/go.yml
name: Go CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.21

      - name: Run tests
        run: go test -v -race ./...

      - name: Run benchmarks
        run: go test -bench=. -benchmem ./...

      - name: Check code coverage
        run: go test -coverprofile=coverage.out ./...

---

プロダクション考慮事項

1. パフォーマンス最適化

メモリアロケーション削減

// シナリオ: 大量のデータ処理

// 非効率な実装
func FilterBad(data []int, threshold int) []int {
    var result []int  // 容量0
    for _, v := range data {
        if v > threshold {
            result = append(result, v)  // 頻繁な再割り当て
        }
    }
    return result
}

// 効率的な実装
func FilterGood(data []int, threshold int) []int {
    // 最悪ケースで全要素が条件を満たすと仮定
    result := make([]int, 0, len(data))
    for _, v := range data {
        if v > threshold {
            result = append(result, v)
        }
    }
    return result
}

// さらに最適化: インプレース処理
func FilterInPlace(data []int, threshold int) []int {
    n := 0
    for _, v := range data {
        if v > threshold {
            data[n] = v
            n++
        }
    }
    return data[:n]  // メモリ再割り当てなし
}

ベンチマーク結果例

BenchmarkFilterBad-8       10000   150000 ns/op   80000 B/op   15 allocs/op
BenchmarkFilterGood-8      50000    35000 ns/op   40000 B/op    1 allocs/op
BenchmarkFilterInPlace-8  100000    15000 ns/op       0 B/op    0 allocs/op

2. セキュリティ考慮事項

バッファオーバーフロー防止

// 危険: 境界チェックなし
func UnsafeAccess(data []int, index int) int {
    return data[index]  // index >= len(data) でパニック
}

// 安全: 境界チェック実施
func SafeAccess(data []int, index int) (int, error) {
    if index < 0 || index >= len(data) {
        return 0, fmt.Errorf("index out of range: %d", index)
    }
    return data[index], nil
}

// さらに安全: デフォルト値を返す
func SafeAccessWithDefault(data []int, index, defaultValue int) int {
    if index < 0 || index >= len(data) {
        return defaultValue
    }
    return data[index]
}

データ検証

// 外部入力の処理
func ProcessUserInput(input []byte) ([]int, error) {
    // 1. サイズ制限
    maxSize := 10000
    if len(input) > maxSize {
        return nil, fmt.Errorf("input too large: %d bytes", len(input))
    }

    // 2. データ解析
    result := make([]int, 0, len(input)/4)

    // 3. 安全な処理
    for i := 0; i < len(input); i += 4 {
        if i+4 > len(input) {
            break  // 境界チェック
        }
        value := bytesToInt(input[i : i+4])
        result = append(result, value)
    }

    return result, nil
}

3. スケーラビリティ

並行処理

// 大量データの並行処理
func ProcessConcurrently(data []int, workers int) []int {
    chunkSize := (len(data) + workers - 1) / workers
    results := make(chan []int, workers)

    // ワーカー起動
    for i := 0; i < workers; i++ {
        start := i * chunkSize
        end := start + chunkSize
        if end > len(data) {
            end = len(data)
        }

        go func(chunk []int) {
            processed := make([]int, len(chunk))
            for i, v := range chunk {
                processed[i] = expensiveOperation(v)
            }
            results <- processed
        }(data[start:end])
    }

    // 結果の集約
    final := make([]int, 0, len(data))
    for i := 0; i < workers; i++ {
        final = append(final, <-results...)
    }

    return final
}

4. モニタリングとロギング

// プロダクショングレードの実装
func ProcessWithMonitoring(data []int) []int {
    start := time.Now()

    // メモリ使用量の追跡
    var memStats runtime.MemStats
    runtime.ReadMemStats(&memStats)
    initialAlloc := memStats.Alloc

    // 処理実行
    result := make([]int, 0, len(data))
    for i, v := range data {
        result = append(result, v*2)

        // 進捗ログ(大量データの場合)
        if i%10000 == 0 && i > 0 {
            log.Printf("Progress: %d/%d (%.1f%%)", i, len(data),
                float64(i)/float64(len(data))*100)
        }
    }

    // パフォーマンスメトリクス
    elapsed := time.Since(start)
    runtime.ReadMemStats(&memStats)
    memUsed := memStats.Alloc - initialAlloc

    log.Printf("Completed: items=%d, time=%v, memory=%d bytes",
        len(data), elapsed, memUsed)

    return result
}

---

なぜこれが重要か

1. キャリア開発との関連

配列・スライスの習得が開く扉:

  • データエンジニアリング: ETL処理、データパイプライン
  • バックエンド開発: API開発、データベース連携
  • DevOps/SRE: ログ分析、メトリクス集計
  • クラウドネイティブ開発: マイクロサービス、コンテナ化
  • 2. 問題解決能力の基礎

    多くのアルゴリズムとデータ構造は配列・スライスの理解が前提:

  • ソートアルゴリズム
  • 検索アルゴリズム
  • 動的計画法
  • グラフアルゴリズム
  • 3. コードの品質向上

    適切な配列・スライス操作は:

  • バグの削減
  • パフォーマンスの向上
  • 保守性の改善
  • チーム生産性の向上

4. チーム協働

コードレビューの視点:

// レビュー前
func Process(data []int) []int {
    var result []int
    for i := 0; i < len(data); i++ {
        result = append(result, data[i]*2)
    }
    return result
}

// レビュー後(改善提案)
// 1. 容量事前確保
// 2. rangeの使用
// 3. ドキュメント追加
func Process(data []int) []int {
    // データを2倍にして返す
    result := make([]int, len(data))
    for i, v := range data {
        result[i] = v * 2
    }
    return result
}

コミュニケーションの例:

> 「このappend操作、ループ内で何度も実行されるので容量を事前確保しませんか? > ベンチマークを取ると約2倍高速化できます。」

5. ソフトスキルの発展

  • 技術的な議論: パフォーマンストレードオフの説明
  • メンタリング: ジュニア開発者への指導
  • ドキュメンテーション: 設計判断の記録
  • プレゼンテーション: 技術選定の説明

---

参考資料

公式ドキュメント

書籍

  • "The Go Programming Language" by Alan A. A. Donovan and Brian W. Kernighan
  • "Concurrency in Go" by Katherine Cox-Buday

オンライン学習

コミュニティ

---

次のセクション: 解答例 | 解説