概要
でも話したcontext.Context
はタイムアウト、キャンセルなどのハンドリングができて便利ですが、使う際に知っておいた方が良いことをいくつかまとめました。
環境
- golang 1.12.0
Tips
Q. 親・子の両方でWithTimeoutが設定されたらどうなるか?
例えば
- 親:4秒
- 子:1秒
の時、もしくはその逆の時どうなるか、です。
func parentFunc(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() err := childFunc(ctx) if err != nil { return err } return nil } func childFunc(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() done := make(chan error, 1) go func() { err := heavyFunc() if err != nil { done <- err } done <- nil }() select { case e := <-done: return e case <-ctx.Done(): return ctx.Err() } }
A. timeoutが短い方が優先される
ドキュメントには以下のように書いてあります。
WithDeadline returns a copy of the parent context with the deadline adjusted to be no later than d. If the parent’s deadline is already earlier than d, WithDeadline(parent, d) is semantically equivalent to parent.
ref: context - The Go Programming Language
つまり短い方のtimeoutになるということですね。なので例の場合は1秒になる
が正解です。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, }
ref: src/context/context.go - The Go Programming Language
実際のソースを確認してもドキュメント通りのロジックであることが分かります。
Q. ctx.Done()はどこでハンドリングすればいいか
cancel()
を呼んだりtimeoutした場合、ctx.Done()
がcloseされるのでzero値がreadできるようになります。
なので
func childFunc(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() done := make(chan error, 1) go func() { err := heavyFunc() if err != nil { done <- err } done <- nil }() select { case e := <-done: return e case <-ctx.Done(): return ctx.Err() } }
のようにselectで待ち受ければtimeoutした時に関数を終了できます。
ではこれを親でもやる必要があるのでしょうか?
また親→子→孫→ひ孫の関係の場合、どこまでctx.Done
ハンドリングすれば良いのでしょうか?
A. 最低限、一番下の子がハンドリングすれば良い。
親→子→孫→ひ孫の関係でcancel()
を呼んだりtimeoutした場合、ひ孫
がctx.Done()
をハンドリングすれば良いです。
そうすれば
ひ孫が終了
↓
孫が終了
↓
子が終了
↓
親が終了
のように順に関数が終了してくれます。
Q. 子のキャンセルは親に伝播するのか?
contextの説明では親のキャンセルが子に伝播する
となっていますが、逆に子でキャンセルしたら親の方でctx.Done()
は発火するのでしょうか?
func childFunc(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() done := make(chan error, 1) go func() { err := childFunc2(ctx) if err != nil { done <- err } done <- nil }() select { case e := <-done: fmt.Println("childFunc done", e) return e case <-ctx.Done(): fmt.Println("childFunc ctx.Done", ctx.Err()) return ctx.Err() } } func childFunc2(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() done := make(chan error, 1) go func() { err := heavyFunc() if err != nil { done <- err } done <- nil }() select { case e := <-done: fmt.Println("childFunc done", e) return e case <-ctx.Done(): fmt.Println("childFunc2 ctx.Done", ctx.Err()) return ctx.Err() } }
parentFunc └ childFunc └ childFunc2
という階層になっています。これでchildFunc2がtimeout
した場合、上のchildFuncのctx.Done()
は発火するのかどうか。
A. しない
実行してみると分かりますが、
childFunc2 ctx.Done context deadline exceeded childFunc done context deadline exceeded
このように上のchildFuncはctx.Done()
の方でなく、done
のchannelから通知が来て終了しています。
Q. timeoutを設定していたらcancelを実行しなくても良いのか?
ドキュメントには
The returned context’s Done channel is closed when the deadline expires, when the returned cancel function is called, or when the parent context’s Done channel is closed, whichever happens first.
ref: context - The Go Programming Language
とあります。つまりtimeoutでもcancelの実行でもどちらでもctx.Done()
はクローズされるのです。
ではなぜWithTimetou()
はcancel functionを返し、
Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.
のように必ず実行するように推奨しているのでしょうか?
A. 他の子に終了を通知するために必要
例えば
parentFunc ├ childFuncA └ childFuncB
で、childFuncA
がtimeoutで終了してもchildFuncB
にはそれによるctx.Done
は伝わりません。
childFuncAでtimeout
↓
parentFuncでcancel()実行
↓
childFuncBにctx.Done()が伝わる
という流れをにする必要があるため、cancel()の実行も必要になります。
またtimeoutだけでなくエラーで即時終了するケースがあります。その際もdefer cancel()
しておけばすぐに他の子に終了通知を送れます。
まとめ
context.Context
で知っておいた方が良いことをまとめました。