Go言語Context完全ガイド: キャンセル・タイムアウト・値の伝播
Go言語のcontextパッケージは、ゴルーチン間でキャンセル信号、デッドライン、リクエストスコープの値を伝播するための標準的な仕組みです。Web APIやデータベースアクセスで必須の知識です。
Contextとは
Contextは、並行処理において以下を管理します。
主な役割:
- キャンセル伝播: 上位の処理がキャンセルされたら、下位も中断
- タイムアウト制御: 時間制限のある処理を実装
- 値の伝播: リクエストIDやユーザー情報などをゴルーチン間で共有
- デッドライン管理: 絶対時刻での処理期限を設定
基本: context.Background と context.TODO
すべてのContextの起点となる2つの関数があります。
package main
import (
"context"
"fmt"
)
func main() {
// context.Background: メイン関数やトップレベルで使用
ctx := context.Background()
// context.TODO: まだどのContextを使うか決まっていないときの一時的な代用
ctxTodo := context.TODO()
processData(ctx)
}
func processData(ctx context.Context) {
fmt.Println("処理開始")
// ctx を下流の関数に渡していく
}
使い分けのルール:
context.Background(): 本番コードのエントリーポイントcontext.TODO(): リファクタリング中の一時的な代用
WithCancel: キャンセル可能なContext
手動でキャンセル可能なContextを作成します。
基本例
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
// ゴルーチンで長時間処理を開始
go longRunningTask(ctx)
// 2秒後にキャンセル
time.Sleep(2 * time.Second)
cancel()
// キャンセルが伝播するまで待機
time.Sleep(1 * time.Second)
}
func longRunningTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("キャンセルされました:", ctx.Err())
return
default:
fmt.Println("処理中...")
time.Sleep(500 * time.Millisecond)
}
}
}
複数ゴルーチンへの伝播
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 必ずcancelを呼ぶ
// 複数のゴルーチンを起動
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
time.Sleep(3 * time.Second)
fmt.Println("全ワーカーをキャンセル")
cancel() // すべてのworkerに伝播
time.Sleep(1 * time.Second)
}
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d 停止\n", id)
return
default:
fmt.Printf("Worker %d 稼働中\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
WithTimeout: タイムアウト制御
指定時間後に自動的にキャンセルされるContextです。
基本例
func main() {
// 3秒のタイムアウト
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // タイムアウト前に終了した場合のリソース解放
result := make(chan string, 1)
go fetchData(ctx, result)
select {
case res := <-result:
fmt.Println("取得成功:", res)
case <-ctx.Done():
fmt.Println("タイムアウト:", ctx.Err())
}
}
func fetchData(ctx context.Context, result chan<- string) {
// 5秒かかる処理(タイムアウトが先に発動する)
time.Sleep(5 * time.Second)
select {
case result <- "データ":
case <-ctx.Done():
fmt.Println("fetchData中断")
}
}
HTTPリクエストへの適用
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := fetchURL(ctx, "https://api.example.com/data")
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("取得成功:", len(data), "bytes")
}
WithDeadline: 絶対時刻での期限設定
WithTimeoutは相対時間ですが、WithDeadlineは絶対時刻を指定します。
func main() {
// 現在時刻から10秒後を期限に設定
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
go processWithDeadline(ctx)
time.Sleep(15 * time.Second)
}
func processWithDeadline(ctx context.Context) {
deadline, ok := ctx.Deadline()
if ok {
fmt.Println("期限:", deadline.Format(time.RFC3339))
}
for {
select {
case <-ctx.Done():
fmt.Println("期限到達:", ctx.Err())
return
default:
fmt.Println("処理中...")
time.Sleep(2 * time.Second)
}
}
}
WithValue: 値の伝播
リクエストスコープの値をゴルーチン間で共有します。
基本例
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
func main() {
ctx := context.Background()
// 値を追加
ctx = context.WithValue(ctx, userIDKey, "user123")
ctx = context.WithValue(ctx, requestIDKey, "req-abc-456")
handleRequest(ctx)
}
func handleRequest(ctx context.Context) {
userID := ctx.Value(userIDKey).(string)
requestID := ctx.Value(requestIDKey).(string)
fmt.Printf("User: %s, Request: %s\n", userID, requestID)
// 下流の関数にctxを渡す
processData(ctx)
}
func processData(ctx context.Context) {
// 上流で設定された値にアクセス可能
userID := ctx.Value(userIDKey).(string)
fmt.Println("処理中のユーザー:", userID)
}
型安全なContext値アクセス
type contextKey string
const userContextKey contextKey = "user"
type User struct {
ID string
Name string
}
// 型安全なセッター
func WithUser(ctx context.Context, user *User) context.Context {
return context.WithValue(ctx, userContextKey, user)
}
// 型安全なゲッター
func GetUser(ctx context.Context) (*User, bool) {
user, ok := ctx.Value(userContextKey).(*User)
return user, ok
}
func main() {
ctx := context.Background()
user := &User{ID: "123", Name: "山田太郎"}
ctx = WithUser(ctx, user)
if u, ok := GetUser(ctx); ok {
fmt.Printf("ユーザー: %s (%s)\n", u.Name, u.ID)
}
}
実践例: Web APIサーバー
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type contextKey string
const requestIDKey contextKey = "requestID"
// ミドルウェア: リクエストIDを生成してContextに追加
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := fmt.Sprintf("%d", time.Now().UnixNano())
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// ミドルウェア: タイムアウト設定
func timeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := ctx.Value(requestIDKey).(string)
log.Printf("[%s] リクエスト受信", requestID)
// データベースクエリのシミュレーション
data, err := fetchFromDB(ctx, requestID)
if err != nil {
if err == context.DeadlineExceeded {
http.Error(w, "タイムアウト", http.StatusRequestTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{
"requestID": requestID,
"data": data,
})
}
func fetchFromDB(ctx context.Context, requestID string) (string, error) {
// 長時間かかるクエリをシミュレート
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
log.Printf("[%s] クエリ中断: %v", requestID, ctx.Err())
return "", ctx.Err()
case <-time.After(500 * time.Millisecond):
log.Printf("[%s] クエリ中... %d/10", requestID, i+1)
}
}
return "取得したデータ", nil
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handler)
// ミドルウェアを適用
var h http.Handler = mux
h = timeoutMiddleware(3 * time.Second)(h)
h = requestIDMiddleware(h)
log.Println("サーバー起動: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", h))
}
ベストプラクティス
1. Contextは第一引数に
// Good
func ProcessData(ctx context.Context, data string) error {
// ...
}
// Bad
func ProcessData(data string, ctx context.Context) error {
// ...
}
2. 必ずcancelを呼ぶ
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // リソースリーク防止
3. Contextは構造体に埋め込まない
// Bad
type Request struct {
ctx context.Context // NG
Data string
}
// Good
func (r *Request) Process(ctx context.Context) error {
// ...
}
4. WithValueは制限的に使う
// Good: リクエストスコープの値
ctx = context.WithValue(ctx, requestIDKey, "req-123")
// Bad: 関数の引数で渡すべき値
ctx = context.WithValue(ctx, "config", config) // NG
5. エラーチェック
select {
case <-ctx.Done():
if ctx.Err() == context.Canceled {
log.Println("キャンセルされました")
} else if ctx.Err() == context.DeadlineExceeded {
log.Println("タイムアウトしました")
}
return ctx.Err()
}
アンチパターン
1. nilContextを渡す
// Bad
ProcessData(nil, "data") // パニックのリスク
// Good
ProcessData(context.Background(), "data")
2. Contextを無視する
// Bad: ctxを受け取っているのに使っていない
func LongTask(ctx context.Context) {
time.Sleep(10 * time.Second)
}
// Good: キャンセルに対応
func LongTask(ctx context.Context) {
select {
case <-time.After(10 * time.Second):
// 処理完了
case <-ctx.Done():
return // キャンセル検知
}
}
3. グローバルなContextを保持
// Bad
var globalCtx context.Context
func init() {
globalCtx = context.Background()
}
// Good: 必要な場所で生成
func main() {
ctx := context.Background()
Process(ctx)
}
まとめ
Contextは、Go言語の並行処理における重要な設計パターンです。
主な使い方:
WithCancel: 手動キャンセルWithTimeout: 相対時間でのタイムアウトWithDeadline: 絶対時刻での期限WithValue: リクエストスコープの値伝播
ベストプラクティス:
- 第一引数にする
- 必ずcancelを呼ぶ
- 構造体に埋め込まない
- WithValueは控えめに
Web APIやデータベースアクセス、マイクロサービス間通信など、あらゆる場面でContextを活用することで、堅牢で保守性の高いGoアプリケーションを構築できます。