フリーキーズ | 独学プログラミング

Go言語のcontextで学ぶGraceful Shutdownとタイムアウト処理

リンドくん

リンドくん

たなべ先生、Go言語でプログラムを書いてるんですけど、処理が長時間動いちゃって止められなくなったことがあるんです...

たなべ

たなべ

あー、それはよくある悩みだね!
Go言語にはcontextという優秀な仕組みがあって、それを使うとGraceful Shutdownといって処理を適切に終了できたり、タイムアウトを設定したりできるんだよ。

プログラミングをしていると、「処理を途中で止めたい」「一定時間で処理を打ち切りたい」という場面に必ず遭遇します。
特にWebアプリケーションやAPIサーバーを開発していると、ユーザーからのリクエストがキャンセルされたり、データベースへの問い合わせが長時間になってしまったりすることがあります。

Go言語では、このような状況をcontextパッケージを使って Gracefulに(優雅に)処理することができます。
contextは、Go言語の並行プログラミングにおける重要な概念の一つで、処理のキャンセルやタイムアウト、値の受け渡しなどを統一的に管理できる仕組みです。

この記事では、Go言語初心者の方でも理解できるよう、contextの基本概念から実践的な使い方まで、段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

contextとは何か?基本概念を理解しよう

リンドくん

リンドくん

contextって、そもそも何なんですか?英語だと「文脈」という意味ですよね?

たなべ

たなべ

その通り!プログラミングでのcontextも「処理の文脈」を表すんだ。
処理がいつキャンセルされるべきかどのくらいの時間で制限すべきかといった情報を持っているんだよ。

contextの役割

Go言語のcontextは、処理の生存期間や制御情報を管理するためのインターフェースです。主に以下の4つの機能を提供します。

  • キャンセル処理 → 処理を途中で安全に停止する
  • タイムアウト制御 → 指定した時間で処理を自動的に終了する
  • デッドライン設定 → 特定の時刻までに処理を完了させる
  • 値の受け渡し → 処理と処理の間における値の共有

この中でも特に重要なのがキャンセル処理タイムアウト制御です。
これらを適切に実装することで、リソースの無駄遣いを防ぎ、アプリケーションの安定性を向上させることができます。

なぜcontextが必要なのか?

従来、Go言語でキャンセル処理を実装する場合、独自のチャネルやフラグを作成する必要がありました。しかし、これでは以下のような問題が発生していました。

  • 実装方法がプロジェクトごとに異なる
  • 複数のゴルーチンでの制御が複雑になる
  • エラーハンドリングが一貫しない

contextパッケージは、これらの問題を解決し、統一された方法でキャンセル処理を実装できるようにしたのです。

contextの基本的な使い方

最もシンプルなcontext

最初に、最もシンプルなcontextの例を見てみましょう。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // バックグラウンドコンテキストを作成
    ctx := context.Background()
    
    // 5秒のタイムアウトを設定
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // 忘れずにキャンセル関数を呼び出す
    
    // 長時間の処理をシミュレート
    processData(ctx)
}

func processData(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("処理が正常に完了しました")
    case <-ctx.Done():
        fmt.Println("タイムアウトまたはキャンセルされました:", ctx.Err())
    }
}

このコードでは、context.WithTimeoutを使って5秒のタイムアウトを設定しています。処理が3秒で完了するため、正常に終了します。

キャンセル処理の実装

次に、手動でキャンセルできる例を見てみましょう。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // キャンセル可能なコンテキストを作成
    ctx, cancel := context.WithCancel(context.Background())

    // 3秒後にキャンセルする
    go func() {
        time.Sleep(3 * time.Second)
        fmt.Println("キャンセルを実行します")
        cancel()
    }()

    // 長時間の処理をシミュレート
    longRunningProcess(ctx)
}

func longRunningProcess(ctx context.Context) {
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("処理がキャンセルされました:", ctx.Err())
            return
        default:
            fmt.Printf("処理中... %d/10\n", i+1)
            time.Sleep(1 * time.Second)
        }
    }
    fmt.Println("すべての処理が完了しました")
}

このコードでは、context.WithCancelを使用して手動でキャンセルできるcontextを作成しています。3秒後にキャンセルが実行され、処理が途中で停止します。

実践的なcontextの活用例

リンドくん

リンドくん

基本はわかりましたが、実際のアプリケーションではどう使うんですか?

たなべ

たなべ

実際のアプリケーションでは、HTTP リクエストデータベースアクセスでよく使われるんだ。具体例を見てみよう。

HTTPクライアントでのタイムアウト

Webアプリケーションでは、外部APIへのリクエストにタイムアウトを設定することが重要です。

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

func fetchData(url string) error {
    // 5秒のタイムアウトを設定
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // contextを使ってリクエストを作成
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return fmt.Errorf("リクエスト作成エラー: %w", err)
    }

    // HTTPクライアントでリクエストを実行
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("リクエスト実行エラー: %w", err)
    }
    defer resp.Body.Close()

    // レスポンスを読み取り
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("レスポンス読み取りエラー: %w", err)
    }

    fmt.Printf("レスポンス: %s\n", string(body[:100])+"...")
    return nil
}

func main() {
    if err := fetchData("https://example.com/some/api"); err != nil {
        fmt.Printf("エラーが発生しました: %v\n", err)
    }
}

複数のゴルーチンでの協調キャンセル

複数のゴルーチンを起動して、一つがエラーになったときに全体を停止する例です。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()

    // ランダムな処理時間をシミュレート
    processingTime := time.Duration(rand.Intn(5)+1) * time.Second

    select {
    case <-time.After(processingTime):
        fmt.Printf("ワーカー %d: 処理完了\n", id)
    case <-ctx.Done():
        fmt.Printf("ワーカー %d: キャンセルされました\n", id)
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup

    // 5つのワーカーを起動
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg)
    }

    // 3秒後にすべてをキャンセル
    go func() {
        time.Sleep(3 * time.Second)
        fmt.Println("タイムアウト: すべてのワーカーをキャンセルします")
        cancel()
    }()

    wg.Wait()
    fmt.Println("すべてのワーカーが終了しました")
}

contextを使う上での注意点とベストプラクティス

重要な注意点

contextを使用する際には、以下の点に注意する必要があります。

  • 必ずdeferでcancel()を呼ぶ → リソースリークを防ぐため
  • contextを構造体のフィールドに保存しない → 関数の引数として渡す
  • contextは関数の第一引数にする → Go言語の慣習

よくある間違い

// ❌ 悪い例 contextを構造体に保存
type BadService struct {
    ctx context.Context
}

// ✅ 良い例 contextを引数として渡す
type GoodService struct{}

func (s *GoodService) ProcessData(ctx context.Context, data string) error {
    // 処理内容
    return nil
}

パフォーマンスの考慮

contextは軽量ですが、不必要に複雑なcontext チェーンを作ることは避けましょう。
シンプルで理解しやすい設計を心がけることが重要です。

まとめ

リンドくん

リンドくん

contextって最初は難しそうに見えましたけど、意外とシンプルですね!

たなべ

たなべ

そうなんだよ!contextは一度覚えてしまえば、Go言語での開発が格段に安全になる重要な仕組みなんだ。ぜひ積極的に使ってみてね。

Go言語のcontextは、処理のキャンセルやタイムアウトを優雅に実装するための強力な仕組みです。今回学んだ内容をまとめると以下の通りです。

contextの主要な機能

  • キャンセル処理context.WithCancelで手動キャンセル
  • タイムアウト制御context.WithTimeoutで時間制限
  • 安全な並行処理 → 複数のゴルーチンでの協調動作

実践での活用場面

  • HTTPクライアントでのリクエストタイムアウト
  • データベースアクセスでの制御
  • 複数のゴルーチンでの協調キャンセル

contextを使いこなすことで、よりロバスト(堅牢)で効率的なアプリケーションを開発できるようになります。
最初は慣れないかもしれませんが、日常的に使っていくうちに自然と身についていきます。

小さなHTTPサーバーやデータ処理プログラムから始めて、徐々に複雑な処理でも活用してみてください。

この記事をシェア

関連するコンテンツ