はじめに
これまでは何気なくcontextを扱っていたので、この機会にcontextとは何で、何ができるのかについて調べてみました。
contextとは
contextパッケージのコメントには以下のように記載されています。
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
https://pkg.go.dev/context
これによると、contextの役割は、異なるプロセル間でデッドライン、キャンセルシグナル、その他のリクエストスコープを伝達することです。
これらは特に複数のゴルーチンをまたいだ処理を行う際に力を発揮します。
また、contextを利用する最も一般的なケースとしては、HTTPサーバが挙げられます。
contextの定義
context.Contextはインタフェースとして定義されています。
1 2 3 4 5 6 | type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } |
contextでキャンセルシグナルを伝達する
contextを使ってキャンセルシグナルを伝達する方法を見ていきます。
cancel関数でキャンセルシグナルを伝達する
context.WithCancel
関数を使うと、親のcontextを引き継いだ子のcontextとcancel関数が返ってきます。このcancel関数を実行することで、子のcontextを渡したゴルーチンに対してキャンセルシグナルを伝達することができます。cancelシグナルを受け取ったゴルーチンは自身を終了させます。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) // ゴルーチンを実行する ch := sub(ctx) fmt.Println(<-ch) // キャンセルシグナルを伝達する cancel() time.Sleep(time.Second) } func sub(ctx context.Context) <-chan int { ch := make(chan int) go func() { defer close(ch) fmt.Println("sub start") for { select { case <-ctx.Done(): // キャンセルシグナルが伝達されたら自身を終了する fmt.Println("sub end") return case ch <- 1: fmt.Println("sub send") } } }() return ch } |
実行ログは以下のとおりです。
1 2 3 4 | sub start sub send 1 sub end |
もちろん、複数のゴルーチンに対してもキャンセルシグナルを伝達することができます。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) ch1 := sub(ctx, 1) ch2 := sub(ctx, 2) fmt.Println(<-ch1) fmt.Println(<-ch2) fmt.Println("send cancel signal") cancel() time.Sleep(time.Second) } func sub(ctx context.Context, n int) <-chan int { ch := make(chan int) go func() { fmt.Printf("sub%d start\n", n) defer close(ch) for { select { case <-ctx.Done(): fmt.Printf("sub%d done\n", n) return case ch <- n: fmt.Printf("sub%d send\n", n) } } }() return ch } |
実行ログは以下のとおりです。
1 2 3 4 5 6 7 8 9 | sub2 start sub1 start sub1 send 1 2 send cancel signal sub2 send sub2 done sub1 done |
このように、複数のゴルーチンに対してもキャンセルシグナルを送ることができます。
キャンセルシグナルの伝達範囲
cancel関数は、そのcontextおよび、そのcontextを親とする子のcontextに対してキャンセルシグナルを伝達します。
これはつまり、子のcontextのcancel関数から親のcontextに対してはキャンセルシグナルが伝達されないことを意味します。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package main import ( "context" "fmt" "time" ) func main() { ctx1 := context.Background() ctx2, cancel := context.WithCancel(ctx1) ctx3, _ := context.WithCancel(ctx2) go func(ctx context.Context) { fmt.Println("ctx1 start") select { case <-ctx.Done(): fmt.Println("ctx1 done") } }(ctx1) go func(ctx context.Context) { fmt.Println("ctx2 start") select { case <-ctx.Done(): fmt.Println("ctx2 done") } }(ctx2) go func(ctx context.Context) { fmt.Println("ctx3 start") select { case <-ctx.Done(): fmt.Println("ctx3 done") } }(ctx3) fmt.Println("send cansel signal to ctx2") cancel() time.Sleep(time.Second) } |
実行ログは以下のとおりです。
1 2 3 4 5 6 | send cansel signal to ctx2 ctx1 start ctx3 start ctx3 done ctx2 start ctx2 done |
このように、1つ目のゴルーチンにはキャンセルシグナルが伝達されていない事がわかります。
子のcontextにキャンセルシグナルを伝達させない
context.WithoutCancel
関数を使うと、親のcontextのキャンセルシグナルを伝達させない子のcontextを作ることができます。
先程のコードのctx3を以下のように生成することで、ctx2のキャンセルシグナルを伝達させないようにすることができます。
1 | ctx3 := context.WithoutCancel(ctx2) |
実行ログは以下のとおりです。
1 2 3 4 5 | ctx1 start send cansel signal to ctx2 ctx3 start ctx2 start ctx2 done |
このように、ctx3に対してctx2のキャンセルシグナルが伝達されていないことがわかります。
contextでデッドラインを伝達する
contextを使ってデッドラインを伝達する方法を見ていきます。
cancel関数でデッドラインを伝達する
context.WithDeadline
関数および、
context.WithTimeout
関数を使うことで、デッドラインを伝達することができます。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) defer cancel() // 明示的にキャンセルシグナルを送ることもできる ch := make(chan int) go func(ctx context.Context) { defer close(ch) for { select { case <-ctx.Done(): fmt.Println("done") return case ch <- 1: } time.Sleep(time.Second) } }(ctx) for { select { case n, ok := <-ch: if ok { fmt.Println(n) } else { fmt.Println("timeout") return } } } } |
実行ログは以下のとおりです。
1 2 3 4 | 1 1 done timeout |
context.WithDeadline
関数では、contextに加えてcancel関数も返却されるので、能動的にキャンセルシグナルを送ることもできます。
ちなみに、
context.WithTimeout
関数は、内部で
context.WithDeadline
関数を呼び出しているだけなので、紹介は割愛します。
contextからデッドラインを確認する
contextにデッドラインが設定されているかどうかは、contextの
Deadline
メソッドで確認することができます。
1 2 3 | ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) deadline, ok := ctx.Deadline() fmt.Printf("deadline: %s, ok: %v\n", deadline.Format(time.RFC3339), ok) |
1 | deadline: 2024-12-20T21:15:54+09:00, ok: true |
デッドラインとキャンセルの補足
contextが返すエラー
contextには
Err
メソッドが定義されており、デッドラインかキャンセルシグナルが伝達された場合に取得できるようになっています。
なお、どちらも伝達されていない場合は、nilが返されます。
contextに定義されたerror型
contextパッケージには2種類のエラーが定義されており、キャンセルシグナルが伝達された場合は
context.Canceled
が返され、デッドラインの場合は
context.DeadlineExceeded
が返されます。
1 2 3 4 5 6 | 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 } func (deadlineExceededError) Temporary() bool { return true } |
任意のerrorを返す
WithCancelCause
関数や
WithDeadlineCause
関数を使って、任意のエラーを返すことができます。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package main import ( "context" "errors" "fmt" "time" ) func main() { ctx := context.Background() ctx1, cancel1 := context.WithCancelCause(ctx) go func(ctx context.Context) { select { case <-ctx.Done(): fmt.Println("ctx1 done") fmt.Println(ctx.Err()) fmt.Println(context.Cause(ctx)) // キャンセルの原因を取得 } }(ctx1) fmt.Println("send cancel signal") cancel1(errors.New("cancel1")) // 任意のエラーを渡せる ctx2, cancel2 := context.WithDeadlineCause(ctx, time.Now().Add(time.Millisecond), errors.New("deadline")) defer cancel2() // context.WithDeadlineCauseの場合はエラーを渡せない go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("ctx2 done") fmt.Println(ctx.Err()) fmt.Println(context.Cause(ctx)) // キャンセルの原因を取得 return } } }(ctx2) time.Sleep(time.Second) } |
実行ログは以下のとおりです。
1 2 3 4 5 6 7 | send cancel signal ctx1 done context canceled cancel1 ctx2 done context deadline exceeded deadline |
このように、
context.Cause
関数を使うことで、渡されたエラーを取得することができます。
キャンセル後に任意の処理を実行する
context.AfterFunc
関数を使うことで、デッドラインまたは、キャンセルシグナルが伝達された後に任意の処理を実行させることができます。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() stop := context.AfterFunc(ctx, func() { fmt.Println("after func") }) defer stop() // stop関数を呼び出すと、AfterFuncが実行されなくなる go func(ctx context.Context) { select { case <-ctx.Done(): fmt.Println("done") } }(ctx) cancel() time.Sleep(time.Second) } |
実行ログは以下のとおりです。
1 2 | after func done |
なお、
cancel
関数を呼ぶ前に
stop
関数を呼べば、
AfterFunc
が呼ばれなくなります。
contextに値を持たせる
contextを使ってリクエストスコープを伝達する方法を見ていきます。
contextにValueを持たせる
context.WithValue
関数を使うことで、contextにValueをもたせることができます。持たせたValueはcontextの
Value
メソッドで取得することができます。
サンプルコードは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package main import ( "context" "errors" "fmt" "time" ) type key struct{} var k = key{} func main() { ctx1 := setValue(context.Background(), "value") ctx2, cancel := context.WithTimeout(ctx1, time.Second) go func(ctx context.Context) { v, err := getValue(ctx) if err != nil { fmt.Println(err) return } fmt.Println(v) select { case <-ctx.Done(): fmt.Println("done") } }(ctx2) cancel() time.Sleep(time.Second) } func setValue(ctx context.Context, v string) context.Context { return context.WithValue(ctx, k, v) // keyはinterface{}型なので、何でも入れられる } func getValue(ctx context.Context) (string, error) { v, ok := ctx.Value(k).(string) // 得られる値はinterface{}型なので、型アサーションが必要 if !ok { return "", errors.New("key not found") } return v, nil } |
実行ログは以下のとおりです。
1 2 | value done |
注意点として、同じキーに値をセットすると上書きされてしまうので、文字列などのかぶる可能性が高いものは避けるのが無難です。サンプルコードのようにキーに指定する型を定義するのが一般的です。
さいごに
普段何気なく使っているcontextの役割や使い方などへの理解が深まりました。
goのバージョンが進むにつれてcontextにも新しい機能が追加されていっているので、今後リリースノートを確認する際には注目したいと思います。