第19章: コンテキスト
はじめに
contextパッケージは、Go 1.7で標準ライブラリに追加された重要な機能です。goroutine間でキャンセルシグナル、タイムアウト、リクエストスコープの値を伝播させるための仕組みを提供します。特に、HTTPサーバーやデータベース操作など、長時間実行される処理の制御に不可欠です。この章では、contextの内部実装を深く理解し、実践的な使用方法を学びます。
Contextインターフェースの内部構造
基本インターフェース
type Context interface {
Deadline() (deadline time.Time, ok bool) // デッドラインを返す
Done() <-chan struct{} // キャンセルチャネル
Err() error // キャンセル理由
Value(key interface{}) interface{} // 値の取得
}
🔑 重要: このシンプルな4つのメソッドだけで、複雑な並行処理の制御を実現します。
実装の階層構造
context.Context (interface)
│
├─ emptyCtx (Background, TODO)
│ │
│ └─ 空の実装(キャンセル不可)
│
├─ cancelCtx (WithCancel)
│ │
│ ├─ done chan struct{}
│ ├─ children map[canceler]struct{}
│ └─ err error
│
├─ timerCtx (WithTimeout, WithDeadline)
│ │
│ ├─ cancelCtx (埋め込み)
│ ├─ timer *time.Timer
│ └─ deadline time.Time
│
└─ valueCtx (WithValue)
│
├─ Context (親)
├─ key interface{}
└─ val interface{}
emptyCtxの実装
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return // ゼロ値を返す(デッドラインなし)
}
func (*emptyCtx) Done() <-chan struct{} {
return nil // nilチャネル(永遠にブロック)
}
func (*emptyCtx) Err() error {
return nil // エラーなし
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil // 値なし
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context { return background }
func TODO() Context { return todo }
💡 設計の理由: Background()とTODO()は実際には同じ型ですが、意味的に異なる用途を示すために分けられています。
キャンセル処理の内部実装
cancelCtxの構造
type cancelCtx struct {
Context // 親コンテキスト
mu sync.Mutex // 以下のフィールドを保護
done chan struct{} // 初回キャンセル時にクローズ
children map[canceler]struct{} // 子コンテキストの集合
err error // キャンセル理由
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
WithCancelの内部動作
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
propagateCancel関数の重要性:
親コンテキスト
│
v
┌──────────────────────────────────────┐
│ 親がキャンセル可能? │
├──────────────────────────────────────┤
│ YES → 親のchildrenマップに追加 │
│ 親キャンセル時に自動キャンセル │
│ │
│ NO → goroutineで親のDone監視 │
│ 親キャンセル時に伝播 │
└──────────────────────────────────────┘
キャンセル伝播の仕組み
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 既にキャンセル済み
}
c.err = err
if c.done == nil {
c.done = closedchan // 事前にクローズされたチャネル
} else {
close(c.done) // Doneチャネルをクローズ
}
// 全ての子をキャンセル
for child := range c.children {
child.cancel(false, err) // 再帰的にキャンセル
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
キャンセル伝播の視覚化:
親コンテキスト
│
├─ 子1 (cancelCtx)
│ ├─ 孫1
│ └─ 孫2
│
└─ 子2 (cancelCtx)
└─ 孫3
親.cancel() 呼び出し
│
├─► 子1.cancel()
│ ├─► 孫1.cancel()
│ └─► 孫2.cancel()
│
└─► 子2.cancel()
└─► 孫3.cancel()
全て同時にDoneチャネルがクローズ
実践例:並行Worker制御
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: 停止 (理由: %v)\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d: 処理中...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// ルートコンテキスト
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
// 5つのworkerを起動
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
// 3秒後に全てキャンセル
time.Sleep(3 * time.Second)
fmt.Println("\n全workerをキャンセル中...")
cancel() // ← 1回の呼び出しで全てのworkerが停止
wg.Wait()
fmt.Println("全worker停止完了")
}
🔑 重要: cancel()は何度呼んでも安全です(冪等性)。2回目以降は何もしません。
タイムアウト処理の深層
timerCtxの実装
type timerCtx struct {
cancelCtx // cancelCtxを埋め込み
timer *time.Timer
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop() // タイマーを停止
c.timer = nil
}
c.mu.Unlock()
}
WithTimeoutの内部フロー
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 親のデッドラインの方が早い → 親と同じ
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // 既に期限切れ
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
タイムアウトの動作フロー:
WithTimeout(parent, 5*time.Second)
│
v
┌──────────────────────────────────┐
│ time.Timer を 5秒後に設定 │
└────────┬─────────────────────────┘
│
├─► 5秒以内に手動cancel()
│ → timer.Stop()
│ → err = context.Canceled
│
└─► 5秒経過
→ timer発火
→ c.cancel(true, DeadlineExceeded)
→ err = context.DeadlineExceeded
親子関係のデッドライン継承
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 親: 10秒のタイムアウト
parentCtx, parentCancel := context.WithTimeout(
context.Background(),
10*time.Second,
)
defer parentCancel()
// 子1: 3秒のタイムアウト(親より早い)
child1Ctx, child1Cancel := context.WithTimeout(parentCtx, 3*time.Second)
defer child1Cancel()
// 子2: 15秒のタイムアウト(親より遅い → 実質10秒)
child2Ctx, child2Cancel := context.WithTimeout(parentCtx, 15*time.Second)
defer child2Cancel()
// 各コンテキストのデッドラインを確認
printDeadline := func(name string, ctx context.Context) {
if deadline, ok := ctx.Deadline(); ok {
fmt.Printf("%s のデッドライン: %v (残り %v)\n",
name,
deadline.Format("15:04:05"),
time.Until(deadline),
)
}
}
printDeadline("親", parentCtx)
printDeadline("子1", child1Ctx)
printDeadline("子2", child2Ctx)
// 実験: 子1は3秒で、子2は10秒でタイムアウト
go func() {
<-child1Ctx.Done()
fmt.Printf("子1 タイムアウト: %v\n", child1Ctx.Err())
}()
go func() {
<-child2Ctx.Done()
fmt.Printf("子2 タイムアウト: %v\n", child2Ctx.Err())
}()
time.Sleep(12 * time.Second)
}
💡 デッドライン継承のルール: 子コンテキストは親よりも長いデッドラインを設定できません。親が先にキャンセルされるため、実質的に親のデッドラインが適用されます。
データベース操作とコンテキスト
クエリタイムアウトの実装
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int
Name string
}
func queryUsersWithTimeout(db *sql.DB, timeout time.Duration) ([]User, error) {
// タイムアウト付きコンテキスト
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// コンテキストを渡してクエリ実行
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
return nil, fmt.Errorf("クエリ失敗: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name); err != nil {
return nil, fmt.Errorf("スキャン失敗: %w", err)
}
users = append(users, user)
}
// タイムアウトチェック
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("タイムアウト: %w", err)
}
return users, nil
}
QueryContextの内部動作:
db.QueryContext(ctx, query)
│
v
┌──────────────────────────────────────┐
│ 1. クエリをDBに送信 │
└────────┬─────────────────────────────┘
│
v
┌──────────────────────────────────────┐
│ 2. goroutineで結果待機 │
│ select { │
│ case <-ctx.Done(): │
│ → クエリキャンセル │
│ → KILL QUERY 送信 (MySQL) │
│ case result := <-resultChan: │
│ → 正常完了 │
│ } │
└──────────────────────────────────────┘
トランザクションのタイムアウト
func transferWithTimeout(db *sql.DB, fromID, toID int, amount float64) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// トランザクション開始
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // エラー時は自動ロールバック
// 送金元から引き落とし
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
amount, fromID,
)
if err != nil {
return fmt.Errorf("引き落とし失敗: %w", err)
}
// 重い処理をシミュレート
time.Sleep(3 * time.Second)
// コンテキストチェック
if err := ctx.Err(); err != nil {
return fmt.Errorf("タイムアウト: %w", err)
}
// 送金先に入金
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, toID,
)
if err != nil {
return fmt.Errorf("入金失敗: %w", err)
}
// コミット
return tx.Commit()
}
🔑 重要: トランザクション中にタイムアウトすると、自動的にロールバックされます。データの一貫性が保たれます。
HTTPサーバーとコンテキスト
リクエストコンテキストのライフサイクル
HTTP リクエスト受信
│
v
┌──────────────────────────────────────┐
│ net/http が自動でコンテキスト作成 │
│ - 親: Background │
│ - キャンセル条件: │
│ 1. クライアント接続切断 │
│ 2. ServeHTTP完了 │
│ 3. http.Server.Shutdown │
└────────┬─────────────────────────────┘
│
v
┌──────────────────────────────────────┐
│ r.Context() でアクセス可能 │
│ - ハンドラー内で使用 │
│ - 子コンテキスト作成可能 │
└────────┬─────────────────────────────┘
│
v
┌──────────────────────────────────────┐
│ ServeHTTP 終了時に自動キャンセル │
└──────────────────────────────────────┘
ハンドラーでのタイムアウト実装
func longRunningHandler(w http.ResponseWriter, r *http.Request) {
// リクエストコンテキストから派生
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
// 結果チャネル
result := make(chan string, 1)
errChan := make(chan error, 1)
// 重い処理を別goroutineで実行
go func() {
// データベースクエリ(時間かかる)
data, err := fetchDataFromDB(ctx)
if err != nil {
errChan <- err
return
}
// 外部API呼び出し(時間かかる)
processed, err := processWithExternalAPI(ctx, data)
if err != nil {
errChan <- err
return
}
result <- processed
}()
// タイムアウトまたは完了を待機
select {
case res := <-result:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"data": res,
})
case err := <-errChan:
http.Error(w, err.Error(), http.StatusInternalServerError)
case <-ctx.Done():
// タイムアウトまたはクライアント切断
http.Error(w, "Request timeout", http.StatusRequestTimeout)
fmt.Printf("リクエストキャンセル: %v\n", ctx.Err())
}
}
クライアント接続切断の検知
func streamingHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
ctx := r.Context()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// クライアントが接続を切った
fmt.Println("クライアント切断:", ctx.Err())
return
case t := <-ticker.C:
// データを送信
fmt.Fprintf(w, "data: Current time: %s\n\n", t.Format("15:04:05"))
flusher.Flush()
}
}
}
💡 ストリーミングレスポンス: サーバーサイドイベント(SSE)やロングポーリングでは、クライアント切断を検知して適切にリソースを解放することが重要です。
値の伝播(WithValue)
valueCtxの内部実装
type valueCtx struct {
Context // 親コンテキスト
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key) // 親に委譲
}
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
値の検索プロセス(連結リスト構造):
Value(key3) を検索
valueCtx(key1=val1)
│
├─► key == key1? NO
│ → 親に委譲
v
valueCtx(key2=val2)
│
├─► key == key2? NO
│ → 親に委譲
v
valueCtx(key3=val3)
│
└─► key == key3? YES ← 発見!
→ return val3
時間計算量: O(n) (nは階層の深さ)
⚠️ パフォーマンス注意: 深い階層で頻繁にValue()を呼ぶとパフォーマンスが低下します。リクエストスコープの少数の値のみに使用してください。
型安全なキー設計
package main
import (
"context"
"fmt"
)
// キー型を独自定義(衝突防止)
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
traceIDKey contextKey = "traceID"
)
// ヘルパー関数で型安全に
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func GetUserID(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey, requestID)
}
func GetRequestID(ctx context.Context) (string, bool) {
requestID, ok := ctx.Value(requestIDKey).(string)
return requestID, ok
}
// 使用例
func handleRequest(ctx context.Context) {
// 値を追加
ctx = WithUserID(ctx, "user123")
ctx = WithRequestID(ctx, "req456")
// 別の関数に渡す
processData(ctx)
}
func processData(ctx context.Context) {
userID, ok := GetUserID(ctx)
if !ok {
fmt.Println("ユーザーID が見つかりません")
return
}
requestID, ok := GetRequestID(ctx)
if !ok {
fmt.Println("リクエストID が見つかりません")
return
}
fmt.Printf("処理中: ユーザー=%s リクエスト=%s\n", userID, requestID)
}
func main() {
ctx := context.Background()
handleRequest(ctx)
}
🔑 ベストプラクティス: 独自の型を定義することで、他のパッケージとのキー衝突を防ぎます。
構造体を使った複雑な値の伝播
package main
import (
"context"
"fmt"
)
type contextKey int
const requestMetadataKey contextKey = 0
// リクエストメタデータ
type RequestMetadata struct {
UserID string
RequestID string
IPAddress string
UserAgent string
StartTime time.Time
}
func WithRequestMetadata(ctx context.Context, meta *RequestMetadata) context.Context {
return context.WithValue(ctx, requestMetadataKey, meta)
}
func GetRequestMetadata(ctx context.Context) (*RequestMetadata, bool) {
meta, ok := ctx.Value(requestMetadataKey).(*RequestMetadata)
return meta, ok
}
// ミドルウェアでメタデータを注入
func requestMetadataMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
meta := &RequestMetadata{
UserID: extractUserID(r),
RequestID: generateRequestID(),
IPAddress: r.RemoteAddr,
UserAgent: r.UserAgent(),
StartTime: time.Now(),
}
ctx := WithRequestMetadata(r.Context(), meta)
r = r.WithContext(ctx)
next(w, r)
}
}
// ハンドラーでメタデータを使用
func handler(w http.ResponseWriter, r *http.Request) {
meta, ok := GetRequestMetadata(r.Context())
if !ok {
http.Error(w, "メタデータなし", http.StatusInternalServerError)
return
}
// ログ出力
log.Printf(
"ユーザー=%s リクエスト=%s IP=%s 処理時間=%v",
meta.UserID,
meta.RequestID,
meta.IPAddress,
time.Since(meta.StartTime),
)
fmt.Fprintf(w, "こんにちは、%s さん", meta.UserID)
}
実践的なパターン
パターン1: 階層的なタイムアウト
func orchestrateServices(ctx context.Context) error {
// 全体で30秒のタイムアウト
globalCtx, globalCancel := context.WithTimeout(ctx, 30*time.Second)
defer globalCancel()
// サービスA: 10秒
serviceACtx, cancelA := context.WithTimeout(globalCtx, 10*time.Second)
defer cancelA()
resultA, err := callServiceA(serviceACtx)
if err != nil {
return fmt.Errorf("サービスA失敗: %w", err)
}
// サービスB: 15秒(サービスAの結果を使用)
serviceBCtx, cancelB := context.WithTimeout(globalCtx, 15*time.Second)
defer cancelB()
resultB, err := callServiceB(serviceBCtx, resultA)
if err != nil {
return fmt.Errorf("サービスB失敗: %w", err)
}
// サービスC: 5秒(AとBの結果を統合)
serviceCCtx, cancelC := context.WithTimeout(globalCtx, 5*time.Second)
defer cancelC()
return callServiceC(serviceCCtx, resultA, resultB)
}
タイムアウトの関係:
全体 (30秒)
├─ サービスA (10秒)
├─ サービスB (15秒)
└─ サービスC (5秒)
実際の制約:
- サービスA: 10秒 (個別)
- サービスB: min(15秒, 30秒-A実行時間)
- サービスC: min(5秒, 30秒-A実行時間-B実行時間)
パターン2: Fan-Out/Fan-In with Context
func parallelFetch(ctx context.Context, urls []string) ([]string, error) {
resultsChan := make(chan string, len(urls))
errorsChan := make(chan error, len(urls))
// Fan-Out: 各URLを並列フェッチ
for _, url := range urls {
go func(u string) {
data, err := fetchWithContext(ctx, u)
if err != nil {
errorsChan <- err
return
}
resultsChan <- data
}(url)
}
// Fan-In: 結果を収集
var results []string
for i := 0; i < len(urls); i++ {
select {
case result := <-resultsChan:
results = append(results, result)
case err := <-errorsChan:
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
return results, nil
}
func fetchWithContext(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()
data, err := io.ReadAll(resp.Body)
return string(data), err
}
パターン3: 早期終了(First Win)
func fetchFastest(ctx context.Context, urls []string) (string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
resultChan := make(chan string, 1)
errorChan := make(chan error, len(urls))
for _, url := range urls {
go func(u string) {
data, err := fetchWithContext(ctx, u)
if err != nil {
errorChan <- err
return
}
select {
case resultChan <- data:
// 最初の成功を送信
case <-ctx.Done():
// 既に他が成功済み
}
}(url)
}
select {
case result := <-resultChan:
// 最初に成功したものを返す
cancel() // 他のgoroutineをキャンセル
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
💡 最適化: 最初のレスポンスを受け取ったら、他のリクエストをすぐにキャンセルすることでリソースを節約します。
エラーハンドリング
context.Err()の使い分け
func processWithContext(ctx context.Context) error {
result, err := longOperation(ctx)
if err != nil {
// コンテキストエラーかチェック
if ctx.Err() == context.Canceled {
return fmt.Errorf("処理がキャンセルされました")
}
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("処理がタイムアウトしました")
}
return fmt.Errorf("処理エラー: %w", err)
}
return nil
}
エラーの種類:
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true } ← net.Errorインターフェース
func (deadlineExceededError) Temporary() bool { return true }
ラップされたエラーの扱い
func robustOperation(ctx context.Context) error {
err := someOperation(ctx)
if err != nil {
// errors.Isでチェック(ラップされていてもOK)
if errors.Is(err, context.Canceled) {
log.Println("ユーザーがキャンセルしました")
return nil // キャンセルは正常終了として扱う
}
if errors.Is(err, context.DeadlineExceeded) {
log.Println("タイムアウトしました")
return ErrTimeout
}
return fmt.Errorf("予期しないエラー: %w", err)
}
return nil
}
パフォーマンスとメモリ管理
コンテキストのメモリオーバーヘッド
emptyCtx:
サイズ: 0バイト (型のみ)
cancelCtx:
- Context: 8バイト (ポインタ)
- mu: 8バイト (sync.Mutex)
- done: 8バイト (chan struct{})
- children: 8バイト (map)
- err: 16バイト (interface{})
合計: 約48バイト + childrenマップのサイズ
timerCtx:
- cancelCtx: 48バイト
- timer: 8バイト (ポインタ)
- deadline: 24バイト (time.Time)
合計: 約80バイト
valueCtx:
- Context: 8バイト (ポインタ)
- key: 16バイト (interface{})
- val: 16バイト (interface{})
合計: 約40バイト
過度なWithValueの問題
// 悪い例: 深いネスト
ctx := context.Background()
for i := 0; i < 1000; i++ {
ctx = context.WithValue(ctx, fmt.Sprintf("key%d", i), i)
}
// 1000層の連結リスト → Value検索が遅い
// 良い例: 構造体にまとめる
type Metadata struct {
Values map[string]interface{}
}
ctx := context.WithValue(context.Background(), metadataKey, &Metadata{
Values: make(map[string]interface{}),
})
// 1層のみ → Value検索が高速
🔑 最適化: 複数の値を伝播する必要がある場合は、構造体にまとめて1つのWithValueで設定します。
自己診断問題
以下の問題に答えて、理解度を確認しましょう:
- 基礎:
context.Background()とcontext.TODO()の違いは何ですか?実装とセマンティクスの観点から説明してください。 - キャンセル伝播: 親コンテキストをキャンセルすると、子コンテキストはどのように停止しますか?内部実装を説明してください。
- Done チャネル: なぜ
Done()はchan struct{}ではなく<-chan struct{}を返すのですか? - タイムアウト:
WithTimeout(parent, 10s)で作った子コンテキストを、手動で5秒後にキャンセルした場合、ctx.Err()は何を返しますか? - デッドライン継承: 親が5秒、子が10秒のタイムアウトを設定した場合、子は実際に何秒でタイムアウトしますか?
- WithValue:
context.WithValue()を10回ネストした場合、Value()の時間計算量はいくつですか? - HTTPリクエスト: HTTPハンドラー内で
r.Context()を取得すると、どのタイミングで自動キャンセルされますか? - データベース:
db.QueryContext(ctx, query)を実行中にコンテキストがキャンセルされると、データベース側では何が起こりますか? - メモリ:
cancelCtxとvalueCtx、どちらがメモリ使用量が多いですか?理由も説明してください。 - エラー:
context.Canceledとcontext.DeadlineExceededの違いを、発生条件とともに説明してください。 - 並行処理: 5つのgoroutineが同じコンテキストを監視している場合、
cancel()を1回呼ぶだけで全て停止できるのはなぜですか? - ベストプラクティス: なぜコンテキストをstructのフィールドに保存してはいけないのですか?
- インターフェース: シンプルな4メソッドで複雑な制御を実現
- キャンセル伝播: 親から子への自動的な伝播メカニズム
- タイムアウト:
time.Timerによる自動キャンセル - 値の伝播: 連結リスト構造による値の検索
- HTTPとの統合: リクエストライフサイクルとの自然な統合
- データベース: クエリキャンセルによるリソース保護
- パフォーマンス: メモリオーバーヘッドと最適化
- エラーハンドリング: キャンセルとタイムアウトの区別
まとめ
本章では、contextパッケージの深層を学びました。
🔑 重要ポイント:
💡 次のステップ: 次章では、これまで学んだすべての知識を統合して、本番環境で使える実践的なアプリケーションを構築します。コンテキストは、そのすべてのレイヤーで活用されます。
⚠️ 本番環境への注意: すべての長時間処理には必ずコンテキストを渡し、適切なタイムアウトとキャンセル処理を実装してください。これにより、システムの堅牢性と応答性が大幅に向上します。