Go言語のcontextは、複数のゴルーチンにまたがる処理へ「もうやめてよい」という合図と締め切りを配る標準パッケージです。最初に迷うのはcontext.Background()とcontext.TODO()のどちらを書くか、次に詰まるのはcancelの扱いと、タイムアウトを過ぎても処理が止まらない現象です。この記事の出力はすべてGo 1.27.2で実行した結果です。掲載コードはpackage文・import文・補助関数を省いた抜粋で、出力は省略前の完全なプログラムから取っています。Go 1.26で変わったsignal.NotifyContextの挙動や、Go 1.24と1.25で入ったテスト向けの機能も扱います。
まとめ
| 場面 | 書き方 |
|---|---|
| 起点(main・初期化・テスト) | context.Background() |
| 渡すべきctxが未定の仮置き | context.TODO() |
| 途中で止める | WithCancel+defer cancel() |
| 締め切りを付ける | WithTimeout/WithDeadline |
| 止めた理由を残す | WithCancelCause+context.Cause |
| 値を運ぶ | リクエストスコープのみ・非公開キー型 |
| Ctrl+C・SIGTERMで止める | signal.NotifyContext |
実務で一番の落とし穴はタイムアウトの誤解です。contextはゴルーチンを強制終了しません。締め切りを過ぎるとDone()のチャネルが閉じるだけで、それを見て処理を抜けるのは受け取った側のコードの責任です。後半の実測では、合図を確認しないループが100ミリ秒の締め切りに対して1.4秒走り続けました。
contextの役割:取り消し・締め切り・値を親から子へ渡す仕組み
Webサーバがデータベース照会と外部API呼び出しを並行で走らせている最中に、クライアントが接続を切ったとします。何も仕込まなければ裏のゴルーチンは最後まで走り、使われない結果のために接続とメモリを抱え続けます。contextは「もう不要になった」という情報を、親から子へ木構造で伝えます。親が取り消されると派生した子も取り消され、子を取り消しても親には伝わりません。例外は後述のWithoutCancelで作った子で、親の取り消しを受け取りません。
実体は4つのメソッドを持つインターフェースです。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
受け取る側はDone()が返すチャネルをselectで監視します。取り消しか締め切り超過が起きるとチャネルが閉じ、Err()はcontext.Canceledかcontext.DeadlineExceededを返します。それまではnilです。関数がcontextを受け取るときは第1引数に置き、名前をctxにするのが公式パッケージの指定で、標準ライブラリ全体がこの形にそろっています。
context.Background()とcontext.TODO()の違い
どちらも空のcontextを返し、取り消されることも締め切りを持つこともありません。取り消し・締め切り・値の振る舞いは同じです。違いは、内部の型が別でfmt.Printlnするとcontext.Backgroundとcontext.TODOのように表示が分かれる点と、読み手に伝える意味です。
Background()を置く4つの場所
公式パッケージがBackground()の用途として挙げるのは、main関数、初期化処理、テスト、受け付けたリクエストの最上位contextの4つです。親となるcontextが存在しない場所で1回だけ作り、以降はすべてそこから派生させます。
func main() {
ctx := context.Background()
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
return fetchAll(ctx)
}
HTTPハンドラの中でcontext.Background()を新たに作るのは誤りです。r.Context()にはクライアント切断で取り消されるcontextが入っているので、それを下流へ渡します。
TODO()の仮置き用途と移行後に残したときの取り消し断絶
context.TODO()は「ここに渡すべきcontextがまだ決まっていない」という印です。公式も「どのContextを使うべきか不明なとき、あるいはまだ利用できないとき」に使うと書いています。既存コードにcontextを後付けする途中で、呼び出し元がまだctxを持っていない中間関数に置く使い方です。
func loadUser(id string) (*User, error) {
return repo.FindUser(context.TODO(), id)
}
移行後もこれが残ると、そこでcontextの連鎖が切れ、上位で取り消しても下流に届きません。タイムアウトを設定したのに効かない不具合の典型的な原因です。迷ってBackground()で埋めると、起点なのか埋め合わせなのかが後から区別できません。仮置きは必ずTODO()で書き、grep -rn "context.TODO()"の結果が0件になった時点を移行完了とします。
WithCancelでゴルーチンを止める実装
1回のcancelによる3つのゴルーチンの停止
context.WithCancelは派生contextと、それを取り消す関数を返します。次のプログラムは3つのワーカーに同じctxを渡し、350ミリ秒後に1回だけcancel()を呼びます。
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; ; i++ {
select {
case <-ctx.Done():
fmt.Printf("worker %d: stop after %d ticks (%v)\n", id, i, ctx.Err())
return
case <-time.After(100 * time.Millisecond):
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
for id := 1; id <= 3; id++ {
wg.Add(1)
go worker(ctx, id, &wg)
}
time.Sleep(350 * time.Millisecond)
cancel()
wg.Wait()
}
worker 2: stop after 3 ticks (context canceled)
worker 3: stop after 3 ticks (context canceled)
worker 1: stop after 3 ticks (context canceled)
3本とも3回目の待機の後に止まり、ctx.Err()はcontext canceledを返しました。停止の順番は実行ごとに変わります。待ち合わせに使っているチャネルとselectの挙動はGoのchannel(チャネル)入門で詳しく扱っています。
defer cancel()を必ず書く理由とgo vetのlostcancel
cancelを呼ばずに関数を抜けた場合に何が残るかは、親の種類で変わります。親が上位のWithCancelやWithTimeoutで作った取り消し可能なcontextなら、親は子への参照を持ち続けます。参照が外れるのは親が取り消されたときか、子がWithTimeoutやWithDeadlineで作られていて自分の期限が来たときです。親がcontext.Background()そのものなら親側に参照は登録されませんが、WithTimeoutやWithDeadlineは設定時刻までタイマーを抱えます。
条件を覚えるより、返り値を受け取った次の行にdefer cancel()を書くと決めるほうが漏れません。2回目以降のcancelは何もしないので、先に明示的に呼んでいても問題ありません。呼び忘れの多くはgo vetのlostcancel検査が検出するので、CIでgo vet ./...を回しておくと取りこぼしが減ります。ただし_ = cancelのように変数を使っただけで呼ばないコードは通ってしまうため、検査は補助と考えます。
WithCancelCauseによる取り消し理由の記録
ctx.Err()が返すのは定型の2種類だけで、なぜ止めたのかは分かりません。ログにcontext canceledとだけ出て原因を追えないのはこのためです。エラー文言から原因を絞る手順はGoのcontext canceledとdeadline exceededの切り分けにまとめています。Go 1.20で入ったWithCancelCauseとCauseを使うと、取り消しに理由のエラーを添えられます。
var errUpstream = errors.New("upstream returned 503")
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errUpstream)
defer cancel(nil)
fmt.Println("Err: ", ctx.Err())
fmt.Println("Cause:", context.Cause(ctx))
fmt.Println("errors.Is(err, Canceled):", errors.Is(ctx.Err(), context.Canceled))
}
Err: context canceled
Cause: upstream returned 503
errors.Is(err, Canceled): true
ctx.Err()は従来どおりcontext.Canceledなので、既存のerrors.Is(err, context.Canceled)の分岐は壊れません。最後のdefer cancel(nil)で理由が消えないのは、取り消し済みのcontextには理由を設定しないとCancelCauseFuncの仕様にあるからです。取り消し前にcancel(nil)を呼んだ場合だけ、Causeはcontext canceledになります(同じプログラムで確認)。
WithTimeoutとWithDeadlineによる締め切りの設定
WithTimeout(parent, d)はWithDeadline(parent, time.Now().Add(d))と同じです。相対時間で書きたいなら前者、「23時59分まで」のように絶対時刻が決まっているなら後者を使います。締め切りを過ぎると自動で取り消され、ctx.Err()はcontext.DeadlineExceededを返します。
HTTPリクエストへの200ミリ秒の締め切り設定
外部API呼び出しにはhttp.NewRequestWithContextを使います。http.ClientのTimeoutと違い、呼び出し元から受け取った締め切りを下流まで引き継げます。次の例では2秒かかるテスト用サーバに200ミリ秒の締め切りで問い合わせます。
func fetch(ctx context.Context, url string) error {
ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
server: request context: context canceled
client: Get "http://127.0.0.1:60596": context deadline exceeded (202ms)
errors.Is DeadlineExceeded: true
client(parent 50ms): Get "http://127.0.0.1:60596": context deadline exceeded (52ms)
出力から2つのことが分かります。1つは、クライアント側がdeadline exceededでもサーバ側のr.Context()はcanceledになることです。もう1つは、親に50ミリ秒の締め切りがあると、子のWithTimeoutが200ミリ秒でも52ミリ秒で打ち切られることです。派生contextの締め切りは親より後ろに延ばせません。だから関数内のWithTimeoutにはcontext.Background()ではなく引数のctxを渡します。
データベース照会とWebフレームワークでのctxの渡し方
database/sqlはQueryContextやExecContextが第1引数にcontextを取り、GORMのGenerics APIのようなORMも終端メソッドにctxを渡せます。ハンドラのr.Context()を流し込めば、クライアント切断時に発行中のクエリへ取り消しが伝わります。実際にクエリが中断されるかはドライバ次第で、取り消しに対応していないドライバではクエリが終わるまで戻りません。
Webフレームワーク独自のコンテキスト型には注意が要ります。Echoのecho.Contextはcontext.Contextを実装していないので、渡せばコンパイルエラーで気づけます。c.Request().Context()で取り出して下流に渡すのが基本形です(Echoの使い方とGin比較)。厄介なのはGinの*gin.Contextで、インターフェースを実装しているのでそのまま渡せてしまいますが、v1.12.0のソースではengine.ContextWithFallbackが既定のfalseのときDone()はnilを返します。コンパイルは通るのに取り消しが届きません。Ginではc.Request.Context()を渡すか、ContextWithFallback = trueを設定します(Gin v1.12.0の本番設定)。
WithTimeoutCauseによる多段タイムアウトの原因識別
リクエスト全体に1秒、その中のDB照会に100ミリ秒のように締め切りが重なると、どちらで切れてもctx.Err()はDeadlineExceededです。Go 1.21で入ったWithTimeoutCauseとWithDeadlineCauseで階層ごとに別のエラーを渡すと、ログだけで原因の階層が分かります。
req, c1 := context.WithTimeoutCause(context.Background(), time.Second, errRequestTimeout)
defer c1()
db, c2 := context.WithTimeoutCause(req, 100*time.Millisecond, errDBTimeout)
defer c2()
<-db.Done()
fmt.Println("nested Err: ", db.Err())
fmt.Println("nested Cause:", context.Cause(db))
fmt.Println("parent Err: ", req.Err())
nested Err: context deadline exceeded
nested Cause: db query exceeded 100ms
parent Err: <nil>
締め切りを過ぎても止まらない処理の直し方
contextは合図を送るだけなので、受け取った側が確認しなければ処理は最後まで走ります。30億回の加算ループに100ミリ秒の締め切りを付け、ctxを一度も見ない版と、100万回ごとにctx.Err()を確認する版を比べました。
func spinChecking(ctx context.Context, n int) (int, error) {
s := 0
for i := 0; i < n; i++ {
if i%1_000_000 == 0 {
if err := ctx.Err(); err != nil {
return s, err
}
}
s += i % 7
}
return s, nil
}
ignoring: returned after 1.434s, ctx.Err()=context deadline exceeded
checking: returned after 103ms, err=context deadline exceeded
確認しない版は締め切りの14倍の1.434秒走り続けました。直し方は処理の種類で決まります。
- CPUを使う長いループ:一定回数ごとに
ctx.Err()を確認して抜ける - 待ちが発生する処理:
selectでctx.Done()と並べて待つ - 外部ライブラリの呼び出し:
QueryContextやNewRequestWithContextのようなctx対応版に置き換える - ctx非対応の古いAPI:別ゴルーチンで呼び、呼び出し側だけ
selectで先に抜ける(裏の処理は走り続ける)
最後の方法は呼び出し元を解放するだけで、裏のゴルーチンはリクエストごとに積み上がります。ctx対応版が無いライブラリは置き換えを検討します。Go 1.27の実装ではErr()はミューテックスを使わないアトミックな読み出しなので、ループ内で頻繁に呼んでも負荷は小さく済みます。
WithValueで値を渡す範囲と独自キー型
WithValueに載せるリクエストスコープの値の範囲
公式パッケージは、WithValueに載せるのは「プロセスやAPIをまたいで運ばれるリクエストスコープのデータ」に限り、関数の任意引数を渡す手段に使わないよう求めています。リクエストID、認証済みユーザーの識別子、トレース情報が典型です。DB接続や設定値のようにリクエストをまたいで共有する依存物は、この記事では構造体のフィールドか明示的な引数で渡す方針を勧めます。依存がシグネチャに現れ、渡し忘れを型で検出できるからです。
WithValueの非公開キー型と値の取得関数
キーは比較可能な型で、パッケージ間の衝突を避けるためstringなどの組み込み型にしないことも公式の指定です。ctx.Value("user_id")のように文字列をキーにすると、別のライブラリが同じ文字列を使ったときに値が入れ替わります。
type ctxKey struct{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, ctxKey{}, id)
}
func RequestID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(ctxKey{}).(string)
return id, ok
}
func main() {
ctx := WithRequestID(context.Background(), "req-42")
ctx = context.WithValue(ctx, "request_id", "overwritten?")
id, ok := RequestID(ctx)
fmt.Println(id, ok)
_, ok = RequestID(context.Background())
fmt.Println("missing:", ok)
}
req-42 true
missing: false
後から"request_id"という文字列キーで値を入れても、非公開型のキーで入れた値は残ります。取り出しはカンマok形式の型アサーションにしているので、値が無くてもpanicせずfalseが返ります。設定と取得を専用関数に閉じ込めておくと、キーの型が外に漏れず、格納する値の型も1か所に固定できます。
Valueの探索段数と取得時間の実測
ctx.Valueは子から親へ1段ずつたどってキーを探します。目的の値の上にWithValueを1段、10段、100段積んだ状態で取り出しを計測した結果が次の表です(Go 1.27.2・Intel Core i9-9880H、go test -bench)。
| 上に積んだWithValue | 1回の取り出し | 割り当て |
|---|---|---|
| 1段 | 15.40 ns | 0 |
| 10段 | 46.57 ns | 0 |
| 100段 | 361.3 ns | 0 |
1段と10段の差は約31ナノ秒で、1リクエストで10回取り出しても差は合計0.3マイクロ秒です。数ミリ秒かかるDB照会や外部API呼び出しを含む処理では表に出ない大きさです。積む段数より先に、載せている値が本当にリクエストスコープのデータかを見直します。
Go 1.21のWithoutCancelとAfterFuncの使いどころ
どちらもGo 1.21(2023年8月8日リリース)で追加されたAPIで、それ以前を前提にした解説には載っていません。
WithoutCancelによる監査ログ書き込みの切り離し
クライアントが切断したら本処理は止めたいが、監査ログの書き込みは最後までやりたい、という場面で使います。context.WithoutCancel(parent)は親の値を引き継いだまま、取り消しと締め切りだけを切り離します。
parent, cancel := context.WithTimeout(context.WithValue(context.Background(), key{}, "req-42"), 10*time.Millisecond)
detached := context.WithoutCancel(parent)
cancel()
audit, cancelAudit := context.WithTimeout(detached, 2*time.Second)
defer cancelAudit()
writeAuditLog(audit)
parent.Err(): context canceled
detached.Err(): <nil>
detached.Done() == nil: true
detached has deadline: false
detached value: req-42
Cause(detached): <nil>
親を取り消しても切り離した側はnilのままで、リクエストIDは残りました。切り離したcontextは締め切りも失うので、そのまま使うと外部の応答が返らないときに永久に待ちます。上の例のように独自のWithTimeoutを付け直すところまでを1セットにします。
AfterFuncによる取り消し時の接続クローズ
context.AfterFunc(ctx, f)は、ctxが取り消された後にfを専用のゴルーチンで実行するよう登録し、登録を解除するstop関数を返します。ctx非対応のnet.Connを取り消しに連動して閉じる用途が典型です。
func serve(ctx context.Context, conn net.Conn) error {
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
defer stop()
return handleConn(conn)
}
stop()の戻り値で、fが動く前に解除できたかが分かります。実測では、取り消し後に呼ぶとfalse、取り消し前に呼ぶとtrueでした。登録時点で取り消し済みならfはすぐ実行されます。stopはfの完了を待たないので、閉じ終わるまで待つ必要があるならチャネルで合図を返します。
シグナルとサーバ停止にcontextを使う実装
signal.NotifyContextとGo 1.26の原因付き取り消し
Ctrl+Cが送るSIGINTや、docker stopが既定で送るSIGTERMで処理を止めるにはsignal.NotifyContextを使います。指定したシグナルが届くとctxが取り消されます。Go 1.26のリリースノートのとおり、1.26からはcontext.Causeでどのシグナルかが取れるようになりました。同じプログラムを2つの版で動かした結果です。
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
fmt.Println("Err: ", ctx.Err())
fmt.Println("Cause:", context.Cause(ctx))
| 版 | ctx.Err() | context.Cause(ctx) |
|---|---|---|
| Go 1.25.0 | context canceled | context canceled |
| Go 1.26.0 | context canceled | terminated signal received |
終了ログにCauseを出しておけば、SIGTERMによる計画停止とアプリ内部の取り消しをログで分けられます。1.25以前で動かす可能性があるコードは、Causeがcontext canceledを返す前提で書きます。
http.Server.Shutdownの終了待機と締め切り
シグナルで止めるときは、処理中のリクエストを返し終えてから終了します。Shutdownは新しい接続の受け付けを止め、処理中のリクエストが終わるまで待ちます。待つ上限はShutdownに渡すctxで決めます。
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
log.Println("signal:", context.Cause(ctx))
stop()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Println("shutdown:", err)
}
300ミリ秒かかるリクエストの処理中にSIGTERMを送ると、Shutdownは280ミリ秒後に戻り、クライアントには200 OKが返りました。shutdownCtxをcontext.Background()から作り直しているのは、シグナルで取り消し済みのctxを渡すとShutdownが待たずにすぐ返るからです。シグナル直後にstop()を呼んでおくと、2回目のCtrl+Cで既定の即時終了に戻ります。上限は、実行環境が強制終了するまでの猶予(Kubernetesの既定は30秒、docker stopの既定はLinuxコンテナで10秒)から、ログの書き出しなど他の終了処理にかかる時間を引いた値にします。猶予を設定で変えている環境では、その値を基準にします。停止処理やタイムアウトの設計を含めてGoで業務システムを組む場合は、フルスクラッチ開発の支援も行っています。
errgroup.WithContextによる最初の失敗での一斉停止
複数のAPIを並行で呼び、1つでも失敗したら残りを止めたいときはgolang.org/x/sync/errgroupのWithContextを使います。標準ライブラリではなく準標準のx/syncモジュールで、最新のv0.24.0(2026年9月23日)はgo.modでgo 1.26.0を要求します。
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
time.Sleep(50 * time.Millisecond)
return errors.New("api A failed")
})
g.Go(func() error {
select {
case <-time.After(2 * time.Second):
return nil
case <-ctx.Done():
fmt.Println("api B stopped:", ctx.Err())
return ctx.Err()
}
})
start := time.Now()
err := g.Wait()
fmt.Printf("Wait: %v (%v)\n", err, time.Since(start).Round(10*time.Millisecond))
fmt.Println("Cause:", context.Cause(ctx))
api B stopped: context canceled
Wait: api A failed (50ms)
Cause: api A failed
Waitを呼んでから50ミリ秒、つまりAが失敗した時点で全体が戻り、Bは2秒待たずに止まりました。Waitが返すのは最初のエラーで、context.Cause(ctx)でも同じエラーが取れます。WithContextが返すctxはWaitから戻った時点で取り消されるので、Waitの後の処理に使い回さないでください。同じキーへの重複呼び出しを1回にまとめたいなら、同じx/syncのsingleflightが担当です。
contextを使うテストの書き方(t.Contextとsynctest)
Go 1.24でT.Contextが入り、テスト内でcontext.Background()を書く必要がなくなりました。このctxはテスト本体の終了後、t.Cleanupに登録した関数の直前に取り消されます。Go 1.25で正式機能になったtesting/synctestと組み合わせると、タイムアウトのテストを実時間で待たずに済みます。
func TestWaitTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
start := time.Now()
err := Wait(ctx, time.Hour)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("got %v", err)
}
t.Logf("fake clock advanced %v", time.Since(start))
})
}
=== RUN TestWaitTimeout
fetch_test.go:20: fake clock advanced 30s
--- PASS: TestWaitTimeout (0.00s)
=== RUN TestContextCanceledBeforeCleanup
fetch_test.go:27: in Cleanup: ctx.Err()=context canceled
30秒の締め切りのテストが0.00秒で通り、偽の時計は30秒進んでいました。synctest.Testの中では、すべてのゴルーチンが「durably blocked」になったときだけ時計が進みます。対象はバブル内で作ったチャネルの送受信、time.Sleep、バブル内でAddしたsync.WaitGroupのWaitなどで、sync.Mutexのロック待ち、ネットワークのI/O、システムコールは含まれません。実ネットワークや外部プロセスを相手にするコードは偽の実装に差し替えてからテストします。
contextを使うべきでない場面
構造体のフィールドに持たせるのは公式が禁じています。インスタンスを複数のリクエストで共有すると、片方の取り消しが無関係な処理を巻き込みます。寿命の違うものを1つのオブジェクトに同居させているのが原因なので、各メソッドの第1引数で渡す形に直します。
nilのcontextを渡すのも禁止です。関数側が許していてもnilは渡さず、決まっていなければcontext.TODO()を渡すよう公式に書かれています。nilのままWithTimeoutなどに渡すとpanicします。
関数の任意引数の代わりに使うと、その関数が何に依存しているかがシグネチャから消え、型チェックも効きません。渡し忘れは実行時のnil参照で初めて表に出ます。必須の値は通常の引数に残せば渡し忘れはコンパイルエラーになります。任意の設定だけをオプション構造体にまとめる場合、フィールドの省略はコンパイラが止めないので、必須項目は関数の先頭で検証します。
エラーの種類の判定にerr == context.Canceledを使うのも避けます。HTTPクライアントなどはエラーを包んで返すので、比較はerrors.Isで行います(Go言語のエラーハンドリング実装ガイド)。
よくある質問
contextはどういう意味で、Goではなぜctxと書くのですか?
英単語のcontextは「文脈」「前後関係」という意味です。Goのcontextは、1つのリクエストを処理している間の取り消し状態・締め切り・付随する値を指します。ctxはその略で、公式パッケージが第1引数にctxという名前で置くよう指定しているため、標準ライブラリもこの書き方にそろっています。
context.TODO()とcontext.Background()に実行時の違いはありますか?
取り消し・締め切り・値の振る舞いに違いはありません。どちらも空のcontextを返し、内部の型と文字列表現(context.Background/context.TODO)だけが異なります。起点にはBackground()、渡すべきctxが決まっていない仮置きにはTODO()を書き、TODO()は移行後に検索して0件にします。
cancelを呼ばないと何が起きますか?
親が取り消し可能なcontextの場合、親が取り消されるか子自身の期限が来るまで子への参照が残り、メモリが解放されません。WithTimeoutやWithDeadlineでは期限までタイマーも残ります。返り値を受け取った直後にdefer cancel()を書き、go vetのlostcancel検査を補助に使います。
context canceledというエラーが出る原因は何ですか?
そのctxか、その親のどれかでcancelが呼ばれたことを意味します。HTTPサーバでは、クライアントの切断やクライアント側のタイムアウトでr.Context()が取り消されたときによく出ます(クライアント側がdeadline exceededでもサーバ側はcanceled)。誰が取り消したかを残したいならWithCancelCauseで理由を渡します。
ctx.Err()とcontext.Cause()の違いは何ですか?
ctx.Err()は取り消し前ならnil、取り消し後はcontext.Canceledかcontext.DeadlineExceededの定型エラーを返します。context.Cause(ctx)はGo 1.20で追加された関数で、WithCancelCauseやWithTimeoutCauseで渡した具体的なエラーを返します。理由が渡されていない場合はctx.Err()と同じ値で、Go 1.26以降のsignal.NotifyContextやerrgroup.WithContextは理由を自動で設定します。