Day 6: 高度なパターン - 解説

概要

Day 6では、Goの並行処理における最も重要な概念の一つであるcontextパッケージと、高度な同期プリミティブ、そしてエラーハンドリングのベストプラクティスを学びました。これらは、プロダクションレベルのGoアプリケーションを構築する上で欠かせない知識です。

---

1. Contextパッケージの重要性

なぜContextが必要なのか?

Contextパッケージは、以下の問題を解決するために設計されました:

  • ゴルーチンの適切な終了: 親処理がキャンセルされた時、関連する全てのゴルーチンを確実に停止させる
  • タイムアウト管理: ネットワーク呼び出しやデータベースクエリに時間制限を設定
  • リクエストスコープ値の伝播: ユーザーID、トレースID、認証情報などをAPI境界を越えて渡す
  • Contextの種類と使い分け

    // 1. Background - プログラムのエントリーポイント
    ctx := context.Background()
    
    // 2. TODO - 一時的なプレースホルダー(後で適切なcontextに置き換える)
    ctx := context.TODO()
    
    // 3. WithCancel - 手動でキャンセル可能
    ctx, cancel := context.WithCancel(parent)
    defer cancel() // 必ず呼ぶ
    
    // 4. WithTimeout - 時間制限付き
    ctx, cancel := context.WithTimeout(parent, 5*time.Second)
    defer cancel()
    
    // 5. WithDeadline - 絶対時刻で制限
    deadline := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(parent, deadline)
    defer cancel()
    
    // 6. WithValue - 値を伝播(推奨されない使い方に注意)
    ctx = context.WithValue(parent, key, value)
    

    Context使用のベストプラクティス

  • 常に第一引数として渡す: func DoSomething(ctx context.Context, arg string)
  • nilを渡さない: 不明な場合はcontext.TODO()を使用
  • WithValueの過度な使用を避ける: リクエストスコープのデータのみに限定
  • 必ずcancelを呼ぶ: リソースリークを防ぐため、defer cancel()を使用

---

2. Syncパッケージの高度な機能

sync.Pool - メモリ効率の向上

sync.Poolは一時的なオブジェクトを再利用するための仕組みです。

使用すべき場面:

  • 頻繁に割り当てと解放が行われるオブジェクト
  • GC圧力を減らしたい場合
  • 高スループットが必要な場合

使用すべきでない場面:

  • 接続プールなど、長期間保持するオブジェクト
  • 状態を持つオブジェクト(必ずリセットが必要)

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

// 使用
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
    buf.Reset() // 重要: 必ずリセット
    bufferPool.Put(buf)
}()

sync.Map - 並行アクセス最適化

通常のmap + Mutexと比較して、以下の場合に高速です:

  • エントリが一度だけ書き込まれ、頻繁に読み取られる
  • 複数のゴルーチンが異なるキーセットを読み書きする

var cache sync.Map

// 書き込み
cache.Store("key", "value")

// 読み込み
value, ok := cache.Load("key")

// 削除
cache.Delete("key")

// 全件走査
cache.Range(func(key, value interface{}) bool {
    // falseを返すと走査を中断
    return true
})

sync.Once - 一度だけの実行保証

シングルトンパターンや初期化処理に最適です。

var (
    instance *Database
    once     sync.Once
)

func GetInstance() *Database {
    once.Do(func() {
        instance = &Database{
            // 初期化処理
        }
    })
    return instance
}

sync.Cond - 条件変数

複雑な同期シナリオで使用します。通常はチャネルの方が推奨されます。

type Queue struct {
    mu    sync.Mutex
    cond  *sync.Cond
    items []int
}

func (q *Queue) Enqueue(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()

    q.items = append(q.items, item)
    q.cond.Signal() // 待機中の1つを起こす
}

func (q *Queue) Dequeue() int {
    q.mu.Lock()
    defer q.mu.Unlock()

    // 条件が満たされるまで待機
    for len(q.items) == 0 {
        q.cond.Wait()
    }

    item := q.items[0]
    q.items = q.items[1:]
    return item
}

---

3. エラーハンドリングのベストプラクティス

エラーラッピング (Go 1.13+)

// %wでエラーをラップ
if err != nil {
    return fmt.Errorf("failed to process: %w", err)
}

// errors.Isで特定のエラーをチェック
if errors.Is(err, ErrNotFound) {
    // 処理
}

// errors.Asで特定の型をチェック
var validationErr *ValidationError
if errors.As(err, &validationErr) {
    // validationErrを使用
}

カスタムエラー型の設計

type MyError struct {
    Op   string // 操作
    Path string // パス
    Err  error  // 原因となったエラー
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s %s: %v", e.Op, e.Path, e.Err)
}

func (e *MyError) Unwrap() error {
    return e.Err
}

センチネルエラー vs カスタムエラー型

センチネルエラー: 事前定義されたエラー値

var ErrNotFound = errors.New("not found")

// 使用
if errors.Is(err, ErrNotFound) {
    // 処理
}

カスタムエラー型: より詳細な情報を含む

type NotFoundError struct {
    Resource string
    ID       int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}

// 使用
var notFoundErr *NotFoundError
if errors.As(err, &notFoundErr) {
    fmt.Printf("Resource: %s, ID: %d\n", notFoundErr.Resource, notFoundErr.ID)
}

---

4. リフレクションの適切な使用

リフレクションを使うべき場面

  • 汎用的なシリアライゼーション: encoding/jsonのような汎用ライブラリ
  • バリデーションフレームワーク: 構造体タグに基づく検証
  • 依存性注入: 型に基づく自動的なワイヤリング
  • テストフレームワーク: モックやアサーションの生成
  • リフレクションを避けるべき場面

  • パフォーマンスが重要な場合: リフレクションは遅い
  • 型安全性が必要な場合: コンパイル時のチェックが効かない
  • 代替手段がある場合: インターフェースや型スイッチの方が良い

リフレクションの基本操作

// 型情報の取得
t := reflect.TypeOf(v)
fmt.Println(t.Kind())    // reflect.Struct, reflect.Int, etc.
fmt.Println(t.Name())    // 型名

// 値情報の取得
val := reflect.ValueOf(v)
fmt.Println(val.IsValid()) // 有効な値かどうか
fmt.Println(val.IsZero())  // ゼロ値かどうか

// 構造体のフィールド操作
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    value := val.Field(i)

    fmt.Printf("%s: %v\n", field.Name, value.Interface())

    // タグの取得
    tag := field.Tag.Get("json")
}

---

5. プロダクションパターン

ワーカープールパターン

ワーカープールは、固定数のゴルーチンでタスクを処理するパターンです。

メリット:

  • ゴルーチン数を制限してリソース使用を制御
  • タスクの並列処理
  • バックプレッシャーの実装

実装のポイント:

  • ジョブキューと結果キューを分離
  • contextでグレースフルシャットダウン
  • WaitGroupで全ワーカーの完了を待機

type WorkerPool struct {
    workerCount int
    jobs        chan Job
    results     chan Result
    ctx         context.Context
    cancel      context.CancelFunc
    wg          sync.WaitGroup
}

// Start: ワーカーを起動
// Submit: ジョブを投入
// Shutdown: グレースフルシャットダウン
// Cancel: 即座にキャンセル

パイプラインパターン

データをステージごとに処理するパターンです。

メリット:

  • 各ステージが独立して並行実行
  • 関心の分離
  • テストが容易

実装のポイント:

  • 各ステージは入力チャネルから読み、出力チャネルに書く
  • contextで全ステージをキャンセル
  • deferでチャネルをクローズ
  • type Stage func(context.Context, <-chan interface{}) <-chan interface{}
    
    func pipeline(ctx context.Context, stages ...Stage) <-chan interface{} {
        out := input
        for _, stage := range stages {
            out = stage(ctx, out)
        }
        return out
    }
    

    ---

    6. 実世界での応用

    Kubernetes

    Kubernetesのクライアントライブラリは、contextを徹底的に使用しています:

    // 全てのAPI呼び出しでcontextを受け取る
    pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
    
    // タイムアウトやキャンセルが自動的に伝播
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    

    Prometheus

    Prometheusは、sync.Poolを使用してメトリクス収集時のGC圧力を軽減:

    var metricPool = sync.Pool{
        New: func() interface{} {
            return &Metric{
                Labels: make(map[string]string),
            }
        },
    }
    
    // メトリクスを取得、使用、返却
    metric := metricPool.Get().(*Metric)
    defer func() {
        metric.Reset()
        metricPool.Put(metric)
    }()
    

    Docker

    Dockerは、contextを使用してコンテナの起動とシャットダウンを制御:

    // コンテナの起動
    resp, err := cli.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, "")
    
    // contextがキャンセルされると、全ての処理が停止
    

    ---

    7. パフォーマンスとベストプラクティス

    Contextのパフォーマンス

  • WithValue: 値ごとに新しいcontextを作成するため、深いチェーンは避ける
  • WithTimeout/WithDeadline: タイマーを作成するため、短命なcontextでは注意
  • sync.Poolのベストプラクティス

  • オブジェクトを返却する前に必ずリセット
  • Poolから取得したオブジェクトは同じゴルーチンで返却
  • GC後にPoolは空になる可能性があることを理解
  • エラーハンドリングのパフォーマンス

  • エラーラッピングは追加のアロケーションを伴う
  • 高頻度のパスではセンチネルエラーを使用
  • errors.Is/Asは線形探索なので、深いチェーンは遅い
  • ---

    8. よくある間違い

    1. Contextを構造体に保存

    // ❌ 悪い例
    type Server struct {
        ctx context.Context
    }
    
    // ✅ 良い例
    func (s *Server) Handle(ctx context.Context) {
        // メソッドの引数として受け取る
    }
    

    2. sync.Poolのオブジェクトをリセットしない

    // ❌ 悪い例
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)
    // リセットしていない!
    
    // ✅ 良い例
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()
    

    3. エラーを無視する

    // ❌ 悪い例
    json.Unmarshal(data, &v)
    
    // ✅ 良い例
    if err := json.Unmarshal(data, &v); err != nil {
        return fmt.Errorf("failed to unmarshal: %w", err)
    }
    

    4. リフレクションの過度な使用

    // ❌ 悪い例(パフォーマンスが重要な場所で)
    v := reflect.ValueOf(obj)
    for i := 0; i < v.NumField(); i++ {
        // 毎回リフレクション
    }
    
    // ✅ 良い例(初期化時に一度だけ)
    type fieldInfo struct {
        index int
        name  string
    }
    var fieldCache []fieldInfo // 初期化時にキャッシュ
    

    ---

    9. まとめと次のステップ

    Day 6で学んだ内容:

  • Context: 並行処理の制御とキャンセル伝播の標準的な方法
  • Sync高度機能: Pool、Map、Once、Condによる効率的な同期
  • エラーハンドリング: ラッピング、カスタム型、適切な分類
  • リフレクション: 動的な型操作と適切な使用場面

これらのパターンは、以下のような実世界のシステムで使用されています:

  • Kubernetes(context、エラーハンドリング)
  • Prometheus(sync.Pool、並行処理)
  • Docker(context、エラーハンドリング)
  • gRPC(context、エラーラッピング)
  • 学習の深化

    さらに学びたい場合は、以下のリソースを参照してください:

  • Go公式ブログ: Context, Error Handling
  • オープンソースプロジェクト: Kubernetes、Prometheus、Dockerのソースコード
  • 書籍: "Concurrency in Go" by Katherine Cox-Buday

Day 7への準備

Day 7では、これまでの知識を統合して実践的なプロジェクトを構築します:

  • RESTful APIサーバー
  • データベース統合
  • ミドルウェアパターン
  • 包括的なテスト

Day 6で学んだcontext、エラーハンドリング、並行処理のパターンは、Day 7のプロジェクトで実践的に使用されます。