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)context.TODO()を使用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, ¬FoundErr) {
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のパフォーマンス
sync.Poolのベストプラクティス
エラーハンドリングのパフォーマンス
---
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で学んだ内容:
これらのパターンは、以下のような実世界のシステムで使用されています:
- 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のプロジェクトで実践的に使用されます。