Context -- キャンセル・タイムアウト・値の伝搬
context.Contextはgoroutine間でキャンセルシグナル・タイムアウト・リクエストスコープの値を伝搬するための標準メカニズムである。
Context -- キャンセル・タイムアウト・値の伝搬
context.Contextはgoroutine間でキャンセルシグナル・タイムアウト・リクエストスコープの値を伝搬するための標準メカニズムである。
この章で学ぶこと
- context.WithCancel -- 手動キャンセル
- context.WithTimeout / WithDeadline -- タイムアウト制御
- context.WithValue -- 値の伝搬とベストプラクティス
- context.AfterFunc (Go 1.21+) -- キャンセル時のコールバック
- context.WithoutCancel (Go 1.21+) -- キャンセル伝搬の切断
- 実践パターン -- HTTPサーバー・DB・マイクロサービスでのContext活用
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- 並行パターン -- Fan-out/Fan-in, Pipeline, Worker Pool の内容を理解していること
1. Contextの基本概念
context.Contextは以下の4つの機能を提供する。
- キャンセル伝搬: 親のキャンセルが全子孫に自動伝搬する
- デッドライン管理: 処理の時間制限を設定する
- 値の伝搬: リクエストスコープの横断的関心事を渡す
- Done()チャネル: キャンセルを検知するためのチャネルを提供する
1.1 Contextの設計原則
- 第一引数に渡す: 関数の第一引数を
ctx context.Contextにする - 構造体に保存しない: リクエストスコープのcontextをフィールドに持たない
- nilを渡さない: 不明な場合は
context.TODO()を使う - 値は横断的関心事のみ: ビジネスロジックのパラメータを入れない
- cancel関数は必ず呼ぶ:
defer cancel()を取得直後に書く
2. context.WithCancel
WithCancelは手動でキャンセルシグナルを送信するためのcontextを生成する。cancel関数を呼ぶと、そのcontextから派生した全ての子contextもキャンセルされる。
コード例 1: context.WithCancel 基本
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// バックグラウンドワーカー
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
return
default:
fmt.Println("working...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
cancel() // goroutineにキャンセルを通知
time.Sleep(100 * time.Millisecond) // goroutineの終了を待つ
}コード例 2: 複数goroutineの同時キャンセル
package main
import (
"context"
"fmt"
"sync"
"time"
)
// worker は定期的にジョブを処理するワーカー
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: stopped (reason: %v)\n", id, ctx.Err())
return
case <-time.After(200 * time.Millisecond):
fmt.Printf("Worker %d: processing\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)
}
// 1秒後に全ワーカーを停止
time.Sleep(1 * time.Second)
fmt.Println("Cancelling all workers...")
cancel()
wg.Wait()
fmt.Println("All workers stopped")
}コード例 3: 条件付きキャンセル
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
var ErrCriticalFailure = errors.New("critical failure detected")
// monitor はシステム状態を監視し、異常検知時にキャンセルする
func monitor(ctx context.Context, cancel context.CancelFunc) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
// ランダムに異常を検知するシミュレーション
if rand.Float64() < 0.05 {
fmt.Println("Monitor: critical failure detected!")
cancel() // 全処理をキャンセル
return
}
}
}
}
// processData はデータを順次処理する
func processData(ctx context.Context) error {
for i := 0; i < 100; i++ {
select {
case <-ctx.Done():
return fmt.Errorf("processing interrupted at item %d: %w", i, ctx.Err())
default:
time.Sleep(50 * time.Millisecond)
fmt.Printf("Processing item %d\n", i)
}
}
return nil
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go monitor(ctx, cancel)
if err := processData(ctx); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("All items processed successfully")
}
}3. context.WithTimeout / WithDeadline
WithTimeoutは指定した期間後に自動的にキャンセルされるcontextを生成する。WithDeadlineは絶対時刻でデッドラインを指定する。内部的にはWithTimeoutはWithDeadlineの薄いラッパーである。
コード例 4: context.WithTimeout
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
// fetchWithTimeout はタイムアウト付きでHTTPリクエストを行う
func fetchWithTimeout(url string, timeout time.Duration) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() // タイムアウト前に完了しても必ずcancelを呼ぶ
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err) // タイムアウト時: context deadline exceeded
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
return body, nil
}
func main() {
body, err := fetchWithTimeout("https://httpbin.org/delay/2", 5*time.Second)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Response: %d bytes\n", len(body))
// タイムアウトするケース
_, err = fetchWithTimeout("https://httpbin.org/delay/10", 3*time.Second)
if err != nil {
fmt.Printf("Expected timeout error: %v\n", err)
}
}コード例 5: context.WithDeadline
package main
import (
"context"
"fmt"
"time"
)
// processUntilDeadline はデッドラインまで処理を続ける
func processUntilDeadline(ctx context.Context) (int, error) {
count := 0
for {
select {
case <-ctx.Done():
return count, ctx.Err() // context.DeadlineExceeded
default:
// 1アイテムの処理をシミュレート
time.Sleep(100 * time.Millisecond)
count++
fmt.Printf("Processed item %d\n", count)
}
}
}
func main() {
// 現在時刻から1秒後をデッドラインに設定
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// デッドラインの確認
if d, ok := ctx.Deadline(); ok {
fmt.Printf("Deadline: %v (in %v)\n", d.Format(time.RFC3339), time.Until(d))
}
count, err := processUntilDeadline(ctx)
fmt.Printf("Processed %d items, error: %v\n", count, err)
}コード例 6: ネストしたタイムアウト
package main
import (
"context"
"fmt"
"time"
)
// 親のタイムアウトが子より短い場合、親のタイムアウトが優先される
func demonstrateNestedTimeout() {
// 親: 2秒タイムアウト
parentCtx, parentCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer parentCancel()
// 子: 5秒タイムアウト(しかし親が2秒でキャンセルするため、実質2秒)
childCtx, childCancel := context.WithTimeout(parentCtx, 5*time.Second)
defer childCancel()
// 孫: 1秒タイムアウト(これが最も短い)
grandchildCtx, grandchildCancel := context.WithTimeout(childCtx, 1*time.Second)
defer grandchildCancel()
// 孫は1秒でタイムアウト
select {
case <-grandchildCtx.Done():
fmt.Printf("Grandchild done: %v\n", grandchildCtx.Err())
}
// 子は親の2秒でタイムアウト(5秒ではない)
select {
case <-childCtx.Done():
fmt.Printf("Child done: %v\n", childCtx.Err())
}
}
func main() {
demonstrateNestedTimeout()
}コード例 7: タイムアウトの残り時間を確認して処理を分岐
package main
import (
"context"
"fmt"
"time"
)
// adaptiveProcess はタイムアウトの残り時間に応じて処理方法を変える
func adaptiveProcess(ctx context.Context) error {
deadline, ok := ctx.Deadline()
if !ok {
// デッドラインが設定されていない
return fullProcess(ctx)
}
remaining := time.Until(deadline)
fmt.Printf("Remaining time: %v\n", remaining)
if remaining < 1*time.Second {
// 残り時間が少ない → 簡易処理
return quickProcess(ctx)
} else if remaining < 5*time.Second {
// 残り時間が中程度 → 標準処理
return standardProcess(ctx)
} else {
// 残り時間が十分 → フル処理
return fullProcess(ctx)
}
}
func quickProcess(ctx context.Context) error {
fmt.Println("Quick process (minimal)")
return nil
}
func standardProcess(ctx context.Context) error {
fmt.Println("Standard process")
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(2 * time.Second):
return nil
}
}
func fullProcess(ctx context.Context) error {
fmt.Println("Full process (comprehensive)")
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(5 * time.Second):
return nil
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := adaptiveProcess(ctx); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("Done")
}
}4. context.WithValue
WithValueはリクエストスコープの値をcontextに格納する。ただし、ビジネスロジックのパラメータではなく、横断的関心事(トレースID、認証情報、ロケール等)のみを格納するべきである。
コード例 8: context.WithValue の基本
package main
import (
"context"
"fmt"
"log"
"net/http"
)
// 独自のキー型を定義してキーの衝突を防ぐ
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
localeKey contextKey = "locale"
)
// requestIDMiddleware はリクエストIDをcontextに設定する
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = generateRequestID() // UUID生成
}
ctx := context.WithValue(r.Context(), requestIDKey, reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// authMiddleware はユーザーIDをcontextに設定する
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
userID, err := validateToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// localeMiddleware はロケール情報をcontextに設定する
func localeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
locale := r.Header.Get("Accept-Language")
if locale == "" {
locale = "ja-JP"
}
ctx := context.WithValue(r.Context(), localeKey, locale)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// handler はcontextから値を取得する
func handler(w http.ResponseWriter, r *http.Request) {
reqID, _ := r.Context().Value(requestIDKey).(string)
userID, _ := r.Context().Value(userIDKey).(int)
locale, _ := r.Context().Value(localeKey).(string)
log.Printf("[%s] User %d, Locale: %s", reqID, userID, locale)
fmt.Fprintf(w, "Hello, user %d!", userID)
}
func generateRequestID() string {
return "req-12345" // 実際にはUUIDを生成する
}
func validateToken(token string) (int, error) {
return 42, nil // 実際にはトークン検証を行う
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/profile", handler)
h := requestIDMiddleware(authMiddleware(localeMiddleware(mux)))
http.ListenAndServe(":8080", h)
}コード例 9: 型安全なContext値アクセサ
package main
import (
"context"
"errors"
"fmt"
)
// --- キー定義 ---
type contextKey int
const (
requestIDKey contextKey = iota
userIDKey
traceIDKey
tenantIDKey
)
// --- 型安全なアクセサ ---
// SetRequestID はリクエストIDをcontextに設定する
func SetRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
// GetRequestID はcontextからリクエストIDを取得する
func GetRequestID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey).(string)
return id, ok
}
// MustGetRequestID はリクエストIDを取得する(存在しなければパニック)
func MustGetRequestID(ctx context.Context) string {
id, ok := GetRequestID(ctx)
if !ok {
panic("requestID not found in context")
}
return id
}
// SetUserID はユーザーIDをcontextに設定する
func SetUserID(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
// GetUserID はcontextからユーザーIDを取得する
func GetUserID(ctx context.Context) (int, error) {
id, ok := ctx.Value(userIDKey).(int)
if !ok {
return 0, errors.New("userID not found in context")
}
return id, nil
}
// SetTraceID はトレースIDをcontextに設定する
func SetTraceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceIDKey, id)
}
// GetTraceID はcontextからトレースIDを取得する
func GetTraceID(ctx context.Context) string {
id, _ := ctx.Value(traceIDKey).(string)
return id // 空文字列がデフォルト
}
func main() {
ctx := context.Background()
ctx = SetRequestID(ctx, "req-abc-123")
ctx = SetUserID(ctx, 42)
ctx = SetTraceID(ctx, "trace-xyz-789")
reqID, _ := GetRequestID(ctx)
userID, _ := GetUserID(ctx)
traceID := GetTraceID(ctx)
fmt.Printf("RequestID: %s, UserID: %d, TraceID: %s\n", reqID, userID, traceID)
}5. Contextの伝搬チェーン
実際のWebアプリケーションでは、HTTPリクエストのcontextを起点として、サービス層、リポジトリ層、外部API呼び出しへとcontextが伝搬する。
コード例 10: HTTPリクエストからDBまでの完全な伝搬チェーン
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// --- Handler層 ---
func handleGetUser(userService *UserService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// HTTPリクエストのContextを基盤にする
ctx := r.Context()
// ハンドラ独自のタイムアウトを追加
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
userID := r.PathValue("id")
user, err := userService.GetUser(ctx, userID)
if err != nil {
switch {
case err == context.Canceled:
// クライアントが接続を切断した
log.Printf("Client disconnected: %v", err)
return
case err == context.DeadlineExceeded:
// タイムアウト
http.Error(w, "request timeout", http.StatusGatewayTimeout)
return
default:
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
}
// --- Service層 ---
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserService struct {
repo *UserRepository
cacheService *CacheService
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
// キャッシュから試行
user, err := s.cacheService.Get(ctx, "user:"+id)
if err == nil && user != nil {
return user, nil
}
// DBから取得
user, err = s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("find user: %w", err)
}
// キャッシュに保存(contextが有効な場合のみ)
if ctx.Err() == nil {
_ = s.cacheService.Set(ctx, "user:"+id, user, 5*time.Minute)
}
return user, nil
}
// --- Repository層 ---
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, fmt.Errorf("query user: %w", err)
}
return &user, nil
}
// --- Cache層 ---
type CacheService struct{}
func (c *CacheService) Get(ctx context.Context, key string) (*User, error) {
// Redis GET with context
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return nil, fmt.Errorf("cache miss") // シミュレート
}
}
func (c *CacheService) Set(ctx context.Context, key string, user *User, ttl time.Duration) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil // シミュレート
}
}
func main() {
// 省略: DB接続、サーバー起動
log.Println("Server starting on :8080")
}コード例 11: マイクロサービス間のContext伝搬
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// contextからHTTPヘッダーへの伝搬
func propagateContext(ctx context.Context, req *http.Request) {
// トレースIDをHTTPヘッダーに伝搬
if traceID := GetTraceID(ctx); traceID != "" {
req.Header.Set("X-Trace-ID", traceID)
}
// リクエストIDも伝搬
if reqID, ok := GetRequestID(ctx); ok {
req.Header.Set("X-Request-ID", reqID)
}
// デッドラインをヘッダーで伝搬(オプション)
if deadline, ok := ctx.Deadline(); ok {
remaining := time.Until(deadline)
req.Header.Set("X-Timeout-Ms", fmt.Sprintf("%d", remaining.Milliseconds()))
}
}
// callExternalService は外部サービスを呼び出す
func callExternalService(ctx context.Context, url string) (map[string]interface{}, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// contextの情報をHTTPヘッダーに伝搬
propagateContext(ctx, req)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("call %s: %w", url, err)
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return result, nil
}
func main() {
ctx := context.Background()
ctx = SetTraceID(ctx, "trace-abc-123")
ctx = SetRequestID(ctx, "req-xyz-789")
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := callExternalService(ctx, "https://httpbin.org/get")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Result: %v\n", result)
}6. context.AfterFunc (Go 1.21+)
Go 1.21で追加されたcontext.AfterFuncは、contextがキャンセルされた後にコールバック関数を実行する。リソースのクリーンアップや通知に使用する。
コード例 12: context.AfterFunc
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
// contextがキャンセルされたらクリーンアップを実行
stop := context.AfterFunc(ctx, func() {
fmt.Println("AfterFunc: context was cancelled, cleaning up...")
// リソースのクリーンアップ処理
closeConnections()
flushLogs()
})
// AfterFuncの戻り値で登録解除が可能
_ = stop // stop()を呼ぶとAfterFuncの登録を解除できる
// 処理を実行
fmt.Println("Processing...")
time.Sleep(1 * time.Second)
// キャンセル → AfterFuncが実行される
cancel()
time.Sleep(100 * time.Millisecond) // AfterFuncの実行を待つ
}
func closeConnections() {
fmt.Println(" Connections closed")
}
func flushLogs() {
fmt.Println(" Logs flushed")
}コード例 13: AfterFuncによるリソース解放パターン
package main
import (
"context"
"fmt"
"sync"
)
// Resource はキャンセル時に自動解放されるリソース
type Resource struct {
name string
mu sync.Mutex
closed bool
}
func (r *Resource) Close() {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return
}
r.closed = true
fmt.Printf("Resource %s: closed\n", r.name)
}
func (r *Resource) Use() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return fmt.Errorf("resource %s is closed", r.name)
}
fmt.Printf("Resource %s: used\n", r.name)
return nil
}
// acquireResource はcontextに紐づくリソースを取得する
func acquireResource(ctx context.Context, name string) *Resource {
r := &Resource{name: name}
// contextキャンセル時にリソースを自動解放
context.AfterFunc(ctx, func() {
r.Close()
})
return r
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
r1 := acquireResource(ctx, "db-conn")
r2 := acquireResource(ctx, "cache-conn")
r1.Use()
r2.Use()
// キャンセル → 全リソースが自動解放
cancel()
// 解放後のアクセスはエラー
if err := r1.Use(); err != nil {
fmt.Printf("Expected error: %v\n", err)
}
}7. context.WithoutCancel (Go 1.21+)
Go 1.21で追加されたcontext.WithoutCancelは、親contextの値は引き継ぐが、キャンセルシグナルは伝搬しない新しいcontextを生成する。バックグラウンド処理やクリーンアップ処理で有用。
コード例 14: context.WithoutCancel
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 親context: 1秒でタイムアウト
parentCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// 値を設定
parentCtx = SetTraceID(parentCtx, "trace-abc")
// WithoutCancel: 親のキャンセルを伝搬しない
backgroundCtx := context.WithoutCancel(parentCtx)
// 値は引き継がれる
fmt.Printf("TraceID in background: %s\n", GetTraceID(backgroundCtx))
// 親がタイムアウトしても影響を受けない
time.Sleep(2 * time.Second)
if parentCtx.Err() != nil {
fmt.Printf("Parent: cancelled (%v)\n", parentCtx.Err())
}
if backgroundCtx.Err() == nil {
fmt.Println("Background: still active!")
}
}コード例 15: WithoutCancelの実践的な利用例
package main
import (
"context"
"fmt"
"log"
"time"
)
// handleRequest はHTTPリクエストを処理する
func handleRequest(ctx context.Context) {
// メイン処理(リクエストcontextに紐づく)
result, err := processRequest(ctx)
if err != nil {
log.Printf("Error: %v", err)
return
}
// 非同期のバックグラウンドタスク(リクエストcontextのキャンセルに影響されない)
bgCtx := context.WithoutCancel(ctx)
// ただし独自のタイムアウトは設定する
bgCtx, bgCancel := context.WithTimeout(bgCtx, 30*time.Second)
go func() {
defer bgCancel()
// 監査ログの記録(リクエストが終了しても完了させたい)
writeAuditLog(bgCtx, result)
// メトリクス送信
sendMetrics(bgCtx, result)
}()
fmt.Println("Request handled, background tasks started")
}
func processRequest(ctx context.Context) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(100 * time.Millisecond):
return "result-data", nil
}
}
func writeAuditLog(ctx context.Context, data string) {
select {
case <-ctx.Done():
log.Printf("Audit log write cancelled: %v", ctx.Err())
case <-time.After(500 * time.Millisecond):
log.Printf("Audit log written: %s", data)
}
}
func sendMetrics(ctx context.Context, data string) {
select {
case <-ctx.Done():
log.Printf("Metrics send cancelled: %v", ctx.Err())
case <-time.After(200 * time.Millisecond):
log.Printf("Metrics sent: %s", data)
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
handleRequest(ctx)
time.Sleep(1 * time.Second) // バックグラウンドタスクの完了を待つ
}8. context.WithCancelCause (Go 1.20+)
Go 1.20で追加されたcontext.WithCancelCauseは、キャンセル理由を付与できるcontextを生成する。デバッグやエラーレポートに有用。
コード例 16: context.WithCancelCause
package main
import (
"context"
"errors"
"fmt"
"time"
)
var (
ErrUserAborted = errors.New("user aborted the operation")
ErrResourceLimit = errors.New("resource limit exceeded")
ErrHealthCheck = errors.New("health check failed")
)
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
// 何らかの条件でキャンセル
time.Sleep(1 * time.Second)
cancel(ErrResourceLimit) // 原因付きでキャンセル
}()
<-ctx.Done()
// キャンセル原因を取得
fmt.Printf("Context error: %v\n", ctx.Err()) // context canceled
fmt.Printf("Cancel cause: %v\n", context.Cause(ctx)) // resource limit exceeded
// 原因に基づいた処理分岐
cause := context.Cause(ctx)
switch {
case errors.Is(cause, ErrUserAborted):
fmt.Println("User chose to abort")
case errors.Is(cause, ErrResourceLimit):
fmt.Println("Resource limit reached, retry later")
case errors.Is(cause, ErrHealthCheck):
fmt.Println("System unhealthy, alerting")
default:
fmt.Printf("Unknown cause: %v\n", cause)
}
}9. Graceful Shutdownとcontext
HTTPサーバーのGraceful Shutdownにおいて、contextは重要な役割を果たす。
コード例 17: Graceful Shutdown
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK")
})
mux.HandleFunc("GET /slow", func(w http.ResponseWriter, r *http.Request) {
select {
case <-r.Context().Done():
log.Println("Client disconnected during slow request")
return
case <-time.After(10 * time.Second):
fmt.Fprintf(w, "Done after 10s")
}
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// サーバーをバックグラウンドで起動
go func() {
log.Printf("Server starting on %s", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// シグナルを待機
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
log.Printf("Received signal: %v, shutting down...", sig)
// Graceful Shutdown: 処理中のリクエストの完了を最大30秒待つ
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("Graceful shutdown failed: %v", err)
// 強制終了
server.Close()
}
log.Println("Server stopped")
}コード例 18: Graceful Shutdown with バックグラウンドワーカー
package main
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// Application はアプリケーション全体のライフサイクルを管理する
type Application struct {
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
func NewApplication() *Application {
ctx, cancel := context.WithCancel(context.Background())
return &Application{ctx: ctx, cancel: cancel}
}
// StartWorker はバックグラウンドワーカーを起動する
func (app *Application) StartWorker(name string, fn func(context.Context)) {
app.wg.Add(1)
go func() {
defer app.wg.Done()
log.Printf("Worker %s: started", name)
fn(app.ctx)
log.Printf("Worker %s: stopped", name)
}()
}
// Shutdown はアプリケーションをシャットダウンする
func (app *Application) Shutdown(timeout time.Duration) {
log.Println("Application: initiating shutdown")
app.cancel()
done := make(chan struct{})
go func() {
app.wg.Wait()
close(done)
}()
select {
case <-done:
log.Println("Application: all workers stopped gracefully")
case <-time.After(timeout):
log.Println("Application: shutdown timeout, some workers may not have stopped")
}
}
func main() {
app := NewApplication()
// メッセージ処理ワーカー
app.StartWorker("message-processor", func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
log.Println("Processing messages...")
}
}
})
// メトリクス収集ワーカー
app.StartWorker("metrics-collector", func(ctx context.Context) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
log.Println("Collecting metrics...")
}
}
})
// ヘルスチェックワーカー
app.StartWorker("health-checker", func(ctx context.Context) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
log.Println("Health check passed")
}
}
})
// シグナル待機
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
app.Shutdown(10 * time.Second)
}10. ASCII図解
図1: Contextツリーの伝搬
context.Background()
│
├── WithCancel ──────────── API Handler
│ │
│ ├── WithTimeout(5s) ── DB Query
│ │
│ └── WithTimeout(3s) ── External API Call
│
└── WithCancel ──────────── Background Worker
│
└── WithValue(traceID) ── Logger
Cancel の伝搬: 親がキャンセル → 全ての子もキャンセル
図2: キャンセル伝搬フロー
クライアント切断
│
▼ ┌───────────────┐
│ HTTP Handler │ ctx.Done() シグナル
│ (ctx) │──────┐
└───────────────┘ ││ ▼
▼ ┌───────────┐| (ctx) |
|---|
| DB Query |
|---|
図3: WithTimeout の内部動作
t=0s t=3s t=5s
│ │ │
├─ ctx作成 ───┤ │
│ Timeout=5s │ │
│ │ │
│ 処理中... │ 処理中... │ ctx.Done()
│ │ │ ← シグナル送信
│ │ │
│ │ ctx.Err() =
│ │ DeadlineExceeded
│ │
│ cancel()で │
│ 早期終了可能 │
図4: ネストしたタイムアウト
t=0s t=1s t=2s t=3s t=4s t=5s
│ │ │ │ │ │
│ ┌────┼───────┼───────┼───────┼───────┤ 親: Timeout=5s
│ │ │ │ │ │ │
│ │ ┌──┼───────┤ │ │ │ 子: Timeout=2s
│ │ │ │ │ │ │ │
│ │ │┌─┤ │ │ │ │ 孫: Timeout=1s
│ │ ││ │ │ │ │ │
│ │ │└─┘ Done │ │ │ │ 孫: 1秒でタイムアウト
│ │ └── Done │ │ │ │ 子: 2秒でタイムアウト
│ └──────────── Done │ │ │ 親: 5秒でタイムアウト
│ │ │ │ │ │
実際: 孫(1s) → 子(2s) → 親(5s) の順でキャンセル
図5: WithoutCancel の動作
parent (WithTimeout 5s)
│
├── child1 (通常)
│ └── 親キャンセル時にキャンセルされる ✓
│
└── child2 (WithoutCancel)
└── 親がキャンセルされても継続 ✗ (キャンセル伝搬なし)
└── 値は引き継ぐ ✓
用途: バックグラウンドタスク、監査ログ記録、メトリクス送信
図6: WithCancelCause の活用
ctx, cancel := context.WithCancelCause(parent)
│
├── cancel(ErrUserAborted)
│ └── context.Cause(ctx) → ErrUserAborted
│
├── cancel(ErrResourceLimit)
│ └── context.Cause(ctx) → ErrResourceLimit
│
└── cancel(nil)
└── context.Cause(ctx) → context.Canceled
デバッグ・エラーレポートに有用
11. 比較表
表1: Context生成関数
| 関数 | 用途 | Done()の発火条件 | Go版 |
|---|---|---|---|
context.Background() |
ルート。main, init, テスト | 発火しない | 1.7+ |
context.TODO() |
未決定の一時的プレースホルダ | 発火しない | 1.7+ |
WithCancel(parent) |
手動キャンセル | cancel()呼び出し | 1.7+ |
WithCancelCause(parent) |
原因付きキャンセル | cancel(err)呼び出し | 1.20+ |
WithTimeout(parent, d) |
時間制限 | d経過 or cancel() | 1.7+ |
WithDeadline(parent, t) |
絶対時刻制限 | t到達 or cancel() | 1.7+ |
WithValue(parent, k, v) |
値の伝搬 | 親に依存 | 1.7+ |
WithoutCancel(parent) |
キャンセル非伝搬 | 発火しない | 1.21+ |
AfterFunc(ctx, fn) |
キャンセル時コールバック | - | 1.21+ |
表2: context.Err() の戻り値
| 状態 | ctx.Err() | ctx.Done() | context.Cause(ctx) |
|---|---|---|---|
| 未キャンセル | nil | ブロック | nil |
| cancel()済み | context.Canceled | クローズ済み | cancel引数 or Canceled |
| タイムアウト | context.DeadlineExceeded | クローズ済み | DeadlineExceeded |
表3: Context値に入れるべきもの / 入れるべきでないもの
| 入れるべきもの | 入れるべきでないもの |
|---|---|
| トレースID / SpanID | ユーザーID(引数で渡す) |
| リクエストID | 検索条件・フィルタ |
| 認証トークン / クレーム | ページネーション情報 |
| ロケール / タイムゾーン | ビジネスロジックのパラメータ |
| ロガーインスタンス | DB接続 / HTTPクライアント |
| テナントID(マルチテナント) | 設定値・フラグ |
表4: Contextの伝搬先
| 伝搬先 | メソッド例 | 重要度 |
|---|---|---|
| database/sql | QueryContext, ExecContext | 必須 |
| net/http | NewRequestWithContext | 必須 |
| gRPC | メタデータ自動伝搬 | 自動 |
| Redis | client.WithContext | 推奨 |
| ログ | logger.WithContext | 推奨 |
| 外部API | NewRequestWithContext | 必須 |
| goroutine | 引数で渡す | 必須 |
12. アンチパターン
アンチパターン 1: contextをstructに保存する
// BAD: contextを構造体のフィールドにする
type Service struct {
ctx context.Context // リクエスト毎に異なるcontextを保持できない
db *sql.DB
}
// GOOD: メソッドの第1引数としてcontextを渡す
type Service struct {
db *sql.DB
}
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
return s.db.QueryRowContext(ctx, "SELECT ...", id).Scan(...)
}アンチパターン 2: WithValueの乱用
// BAD: ビジネスロジックのパラメータをcontextに入れる
ctx = context.WithValue(ctx, "userID", 42)
ctx = context.WithValue(ctx, "orderID", 100)
ctx = context.WithValue(ctx, "limit", 50)
// GOOD: 関数の引数で渡す。contextは横断的関心事のみ
func GetOrders(ctx context.Context, userID, limit int) ([]Order, error) {
// contextにはトレースID・認証情報など横断的関心事のみ
traceID := ctx.Value(traceIDKey).(string)
// ...
}アンチパターン 3: cancel関数を呼ばない
// BAD: cancel関数を呼ばない → リソースリーク
func processRequest(parentCtx context.Context) {
ctx, _ := context.WithTimeout(parentCtx, 5*time.Second)
// cancel が呼ばれない → タイマーgoroutineがリーク
doWork(ctx)
}
// GOOD: defer cancel() を即座に書く
func processRequest(parentCtx context.Context) {
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel() // 必ずリソースを解放
doWork(ctx)
}アンチパターン 4: 文字列キーの使用
// BAD: 文字列をキーに使う(衝突リスク)
ctx = context.WithValue(ctx, "userID", 42)
ctx = context.WithValue(ctx, "userID", "conflict!") // 別パッケージが同じキーを使う可能性
// GOOD: 独自の非公開型をキーに使う
type contextKey int
const userIDKey contextKey = 0
ctx = context.WithValue(ctx, userIDKey, 42)アンチパターン 5: context.Background()の乱用
// BAD: 親contextを無視してBackgroundを使う
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
// 引数のctxを無視してBackgroundを使う → キャンセル・タイムアウトが効かない
dbCtx := context.Background()
return s.db.QueryRowContext(dbCtx, "SELECT ...", id).Scan(...)
}
// GOOD: 受け取ったcontextをそのまま伝搬
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
return s.db.QueryRowContext(ctx, "SELECT ...", id).Scan(...)
}アンチパターン 6: Contextのキャンセル後にリソースを使う
// BAD: contextキャンセル後にレスポンスを書く
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := longRunningTask(ctx)
if err != nil {
// contextがキャンセルされていてもResponseWriterに書き込む
http.Error(w, err.Error(), 500) // クライアント切断時は無意味
return
}
json.NewEncoder(w).Encode(result)
}
// GOOD: contextの状態をチェックしてから応答
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := longRunningTask(ctx)
if ctx.Err() != nil {
// クライアントは既に切断 → 応答不要
log.Printf("Client disconnected: %v", ctx.Err())
return
}
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(result)
}実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
トラブルシューティング
よくあるエラーと解決策
| エラー | 原因 | 解決策 |
|---|---|---|
| 初期化エラー | 設定ファイルの不備 | 設定ファイルのパスと形式を確認 |
| タイムアウト | ネットワーク遅延/リソース不足 | タイムアウト値の調整、リトライ処理の追加 |
| メモリ不足 | データ量の増大 | バッチ処理の導入、ページネーションの実装 |
| 権限エラー | アクセス権限の不足 | 実行ユーザーの権限確認、設定の見直し |
| データ不整合 | 並行処理の競合 | ロック機構の導入、トランザクション管理 |
デバッグの手順
- エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
- 再現手順の確立: 最小限のコードでエラーを再現する
- 仮説の立案: 考えられる原因をリストアップする
- 段階的な検証: ログ出力やデバッガを使って仮説を検証する
- 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
# ロガーの設定
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
def debug_decorator(func):
"""関数の入出力をログ出力するデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"戻り値: {func.__name__} -> {result}")
return result
except Exception as e:
logger.error(f"例外発生: {func.__name__}: {e}")
logger.error(traceback.format_exc())
raise
return wrapper
@debug_decorator
def process_data(items):
"""データ処理(デバッグ対象)"""
if not items:
raise ValueError("空のデータ")
return [item * 2 for item in items]パフォーマンス問題の診断
パフォーマンス問題が発生した場合の診断手順:
- ボトルネックの特定: プロファイリングツールで計測
- メモリ使用量の確認: メモリリークの有無をチェック
- I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
- 同時接続数の確認: コネクションプールの状態を確認
| 問題の種類 | 診断ツール | 対策 |
|---|---|---|
| CPU負荷 | cProfile, py-spy | アルゴリズム改善、並列化 |
| メモリリーク | tracemalloc, objgraph | 参照の適切な解放 |
| I/Oボトルネック | strace, iostat | 非同期I/O、キャッシュ |
| DB遅延 | EXPLAIN, slow query log | インデックス、クエリ最適化 |
13. FAQ
Q1: context.Background()とcontext.TODO()の違いは?
機能的には同じだが意図が異なる。Background()は「ルートcontextとして意図的に使う」、TODO()は「適切なcontextがまだ不明で後で修正する」場合に使う。linterでTODO()を検出して漏れを防げる。
Q2: cancel関数は必ず呼ぶ必要があるか?
はい。WithCancel/WithTimeout/WithDeadlineのcancel関数を呼ばないとリソースリークが発生する。defer cancel() を取得直後に書くのが慣例。タイムアウトで自動キャンセルされてもcancel()は安全に呼べる(何度呼んでもエラーにならない)。
Q3: contextの値はどのような場面で使うべきか?
リクエストスコープの横断的関心事のみ: トレースID、認証情報、ロケール等。ビジネスロジックのパラメータには使わない。独自のキー型(type contextKey string)を定義してキーの衝突を防ぐ。
Q4: context.WithTimeout と http.Client.Timeout のどちらを使うべきか?
両者は補完的である。http.Client.Timeoutはクライアント全体のタイムアウト(接続~レスポンス読み取りまで)を設定する。context.WithTimeoutはリクエスト単位で異なるタイムアウトを設定でき、キャンセル伝搬もサポートする。一般的には両方を設定し、http.Client.Timeoutを安全策として長めに、contextのタイムアウトをリクエスト固有の値に設定する。
Q5: DBトランザクション中にcontextがキャンセルされたらどうなるか?
database/sqlはcontextのキャンセルを検知してクエリを中断する。ただし、トランザクションの状態はドライバ依存。一般的には:
- 実行中のクエリは中断される
- コミット前ならロールバックされる
defer tx.Rollback()パターンで安全にクリーンアップする
Q6: contextをgoroutineに渡すときの注意点は?
- 値をコピーしてからgoroutineに渡す(gin.Contextなど、リクエスト終了後に無効になるオブジェクトに注意)
- バックグラウンドgoroutineには
context.WithoutCancelを検討する - 独自のタイムアウトを設定する
- goroutineリークを防ぐため、必ずキャンセル経路を確保する
Q7: Go 1.21のAfterFuncとWithoutCancelはどう使い分けるか?
AfterFunc: キャンセル時に特定のクリーンアップ処理を実行したい場合(リソース解放、ログ記録等)WithoutCancel: 親のキャンセルに影響されずに処理を続行したい場合(バックグラウンドタスク、監査ログ等)
Q8: テストでcontextをどう扱うべきか?
テストではcontext.Background()に適切なタイムアウトを設定する。デッドラインの長いテストはCI環境でのフレーク(不安定なテスト)の原因になるため、タイムアウトは短めに設定する:
func TestGetUser(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := service.GetUser(ctx, 1)
if err != nil {
t.Fatalf("GetUser: %v", err)
}
// ...
}FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 概念 | 要点 |
|---|---|
| Context | goroutine間のキャンセル・タイムアウト・値伝搬 |
| WithCancel | 手動キャンセル制御 |
| WithCancelCause | 原因付きキャンセル (Go 1.20+) |
| WithTimeout | 時間制限付き処理 |
| WithDeadline | 絶対時刻によるデッドライン |
| WithValue | 横断的関心事の伝搬のみに使う |
| WithoutCancel | キャンセル伝搬を切断 (Go 1.21+) |
| AfterFunc | キャンセル時コールバック (Go 1.21+) |
| cancel() | 必ずdefer cancel()で呼ぶ |
| 伝搬 | 親キャンセル → 全子孫にキャンセル伝搬 |
次に読むべきガイド
- ../02-web/00-net-http.md -- HTTPサーバーでのContext活用
- ../02-web/02-database.md -- DBクエリのContext制御
- ../02-web/03-grpc.md -- gRPCのContext
参考文献
- Go Blog, "Go Concurrency Patterns: Context" -- https://go.dev/blog/context
- Go Standard Library: context -- https://pkg.go.dev/context
- Go Blog, "Contexts and structs" -- https://go.dev/blog/context-and-structs
- Go 1.20 Release Notes (WithCancelCause) -- https://go.dev/doc/go1.20
- Go 1.21 Release Notes (AfterFunc, WithoutCancel) -- https://go.dev/doc/go1.21