Go: context.AfterFunc でキャンセル時の後始末を登録する(見張り用 goroutine が要らない)
Go 1.21 で入った context.AfterFunc(ctx, f) は、ctx が終わった(キャンセルまたは期限切れ)ときに一度だけ f を別の goroutine で実行する。戻り値の stop で登録を取り消せる。
使う場面
conn.Read のように context を受け取らないブロッキング処理を、キャンセルに合わせて止めたいとき。これまでは go func() { <-ctx.Done(); conn.Close() }() のような見張り役を自分で立て、処理が正常に終わったときにその goroutine を確実に終わらせる工夫も必要だった。AfterFunc ならその両方を標準ライブラリに任せられる。
やり方
go
func readWithContext(ctx context.Context, conn net.Conn, buf []byte) (int, error) {
// ctx が終わったら接続を閉じて、ブロック中の Read を戻す
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
defer stop() // 正常に読み終えたら登録を外す(f は呼ばれない)
n, err := conn.Read(buf)
if ctx.Err() != nil {
return n, ctx.Err() // 閉じられたのはキャンセルのせい
}
return n, err
}- 接続を閉じたくない場合は、
fの中でconn.SetReadDeadline(time.Now())を呼んで Read だけ戻す書き方もある。 stop()は「fの実行を止められたか」を bool で返す。false ならfはすでに走り始めている(または ctx がもう終わっていた)。
注意点
- ctx が登録時点ですでに終わっていると、
fはすぐに(別の goroutine で)実行される。 fは呼び出し側とは別の goroutine で動く。fの中で触る値は、データ競合にならないようにする。stop()が true を返したあとはfが呼ばれないことが保証されるが、false のときにfの終了を待ってはくれない。終わりを待つ必要があるならチャネルなどで自分で合図する。- 関連: 外向きの HTTP 呼び出しの期限は Go: http.Get / http.DefaultClient はタイムアウトなし。自前の http.Client に Timeout を付ける、サーバー停止時の後始末は Go: graceful shutdown で処理中のリクエストが落ちるなら、main を Shutdown が終わるまで待たせる。