なにこれ?
golang使ってるとよく出会うことになるであろう、context.Contextの使い方と
contextのgodocの自分的な和訳。
よく登場する割にあまり使わないので忘れがちなので、大雑把に調べ上げたものを載せとく。
(2回忘れたので観念した
contextとは
blog.golang.org/contextの
紹介が一番わかりいいかなぁという気もするのでそのまま使います。
In Go servers, each incoming request is handled in its own goroutine.
Request handlers often start additional goroutines to access backends such as databases and RPC services.
The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user,
authorization tokens, and the request's deadline.
When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.
At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals,
and deadlines across API boundaries to all the goroutines involved in handling a request.
(略
- Goのサーバーで入ってきた各リクエストは個別のgoroutineで処理される。
- リクエストのハンドラは、データベースやRPCのサービスにアクセスする。
- リクエスト上で動作するgoroutineはユーザー識別子や認証トークン、リクエストの期限みたいな特定のリクエストの値にアクセスする必要がある。
- リクエストがタイムアウトされた時、システムがそのリソースを再度利用できるように、すべての動作しているゴルーチンも素早く終了されるべき。
- Googleではそんなリクエストスコープな値とかキャンセルシグナル,デッドラインやらをリクエストに関係する全てのゴルーチンに渡せるcontextパッケージを開発したよ。
より詳細な内容として紹介されているgodocの序文を以下に和訳
context packageはデッドラインやキャンセルのシグナル、その他のリクエストスコープな値をAPIの境界を超えて
プロセス間で届けるContext型を定義している。
サーバーに入ってくるリクエストはContextを作成するべきだし、サーバーへの発信呼び出しはContextを受け入れるべき。
リクエスト着信〜レスポンス発信の間の連鎖する関数の呼び出しの間、Contextは伝播されるべき。
必要があれば、WithCancel,WithDeadLine,WithTimeout,WithValueを使うことで派生するContextに置き換えても良い。
親のContextがキャンセルされた時、全ての派生先のContextもまたキャンセルされる。
WithCancel,WithDeadLine,WithTimeoutのような関数は、Contextを受け取り、
受け取ったContextから派生させた子のContextと、CancelFuncを返します。
CancelFuncを呼ぶことで返されたchildContextとそこから派生したchildrenContextはCancelされ、
派生元(parent)のContextからのchildContextへの参照を取り外し、関連するタイマーをストップします。
CancelFuncの呼び出しを失敗すると、 親がキャンセルされるか、タイマーが発火されるまで、その子と子孫がリークすることになります。
go vetツールはCancelFuncが全てのコントロールフローでCancelFuncが使われていることをチェックしてくれます。
Contextを使うプログラムは、contextの伝播をチェックする静的解析が可能にし
パッケージをまたいだインターフェースの一貫性の維持をするために、以下のルールを守る必要がある。
- 構造体の中には、Contextを保持してはならない。Contextが必要な関数には明示的に渡す事。Contextは第1引数であるべきで、だいたいはctxと名付けてるよ。
- 関数側が許容するとしても、nilのContextを渡してはいけない。どのコンテキスト渡して良いか確証が持てない時は、Context.TODOを使って空のContextを渡してね。
- ContextのValueはAPIやプロセスをまたぐリクエストスコープな値だけに使う。オプショナルな値を関数に渡すためではない。
- 同じContextは別々に実行されているgoroutineで関数渡しても良い。Contextは複数のgoroutineから同時に使われても安全である。
あんまり英語は得意ではないので、ニュアンスやらなんか違うところあったら修正リクエストとかくれると幸せになります :)
よするにgoroutineには親子関係って概念がないので、goroutineを利用してさばいてるwebServerのような処理は、
大元の処理がキャンセルされたら、派生されたゴルーチンの処理もキャンセルしようね、って話。
使い方
まとめると、要するに主なユースケースは以下の通りになります。
-
- リクエストスコープな値の伝播
-
- キャンセル
context.Context interface
goblogの説明を見ると大雑把に以下な感じ。
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
- Contextがキャンセル、タイムアウトされた時に閉じられるチャネルを返すDone()
- Doneチャネルが閉じられた時になぜ閉じられたか(キャンセルされたか)をerror構造体でとり出せるErr()
- DeadLineが設定されていればDeadLineを返すDeadLine()
- Keyに対応した保管値を返すValue()
が提供されてる。
生成
以下の2つの関数で生成可能です。
他にhttp.Request.Context()でRequestからも受け取れる。
Background()
基本的にはこちらを利用。
ctx := context.Background()
TODO()
説明文を見た感じ、使うか使わないかわからないにしてもnilは渡してほしくないから
そういう場合は明示的にTODOで空のcontextを作ろうね、っていう説明がなされています。
動作的にはbackgroundと同じく空のcontextを返します。
ctx := context.TODO()
リクエストスコープの値の伝播
なんでも入れられるからといって、なんでも入れるとただのカオスな運び屋になってしまうので
送るものはgoblog例にあがってたみたいな最小(AuthとかDeadLineみたいな)に留めた方が良いかなぁとよく思ってたりしますが、
そこらへんはまぁお好みというか決めで。使い方はシンプルです。
値のSet WithValue()
値のセットは以下のように行う。
ctx := context.TODO() //生成方法は任意
ctx = context.WithValue(ctx,key,val)
値のGet Value()
値のGetは以下の通り。
val := ctx.Value(key)
キャンセル処理
キャンセル処理とはなんぞや、という感じだと思うので、関数の紹介をしつつ
簡単なコードを。
WithCancel(parent Context) (ctx Context,cancel CancelFunc)
親となるContextを渡すと、子のContextとキャンセル用の関数を作ってくれる。
第2返り値のcancelFuncで返された子のContextをcancelできる。
WithTimeout
WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
timeoutの指定付きのWithCancel。第2引数で指定した時間が経過
とcancelが走る。
WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
基本的にWithTimeoutと同じ。違いは指定した時間が経過
ではなく指定した時刻になったら
なだけ。
サンプル
contextInterfaceのDone()とcancelを使うと以下な感じに処理がキャンセルできる。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
go parentProcess(ctx)
time.Sleep(1000 * time.Second)
}
func parentProcess(ctx context.Context) {
childCtx, cancel := context.WithCancel(ctx)
childCtx2, _ := context.WithCancel(childCtx)
go childProcess(childCtx, "child1")
go childProcess(childCtx2, "child2")
//10秒まってキャンセル。
time.Sleep(10 * time.Second)
// childCtxから派生したchildCtx2もキャンセルが伝播される。実際にはchildCtx2のcancelFuncもどこかで呼ばないとgo vet error
cancel()
//10秒後にキャンセル サンプル用にcancelFuncを捨ててるけど実際にはcancelFuncを明示的にどこかで呼ばないとgo vet error
ctxWithTimeout10, _ := context.WithTimeout(ctx, time.Second*10)
go childProcess(ctxWithTimeout10, "with timeout")
}
func childProcess(ctx context.Context, prefix string) {
for i := 1; i <= 1000; i++ {
select {
case <-ctx.Done():
fmt.Printf("%s: canceld \n", prefix)
return
case <-time.After(1 * time.Second):
fmt.Printf("%s:%d sec..\n", prefix, i)
}
}
}
実行結果
child2:1 sec..
child1:1 sec..
child2:2 sec..
child1:2 sec..
child2:3 sec..
child1:3 sec..
child1:4 sec..
child2:4 sec..
child1:5 sec..
child2:5 sec..
child1:6 sec..
child2:6 sec..
child2:7 sec..
child1:7 sec..
child1:8 sec..
child2:8 sec..
child2:9 sec..
child1:9 sec..
child1: canceld
child2: canceld
with timeout:1 sec..
with timeout:2 sec..
with timeout:3 sec..
with timeout:4 sec..
with timeout:5 sec..
with timeout:6 sec..
with timeout:7 sec..
with timeout:8 sec..
with timeout:9 sec..
with timeout: canceld
参考
blog.golang.org/context
godoc doc/go1.7/context
追記
構造体の中には、Contextを保持してはならない
ここの理由が抜けていたのですが、各Contextに基づく処理(キャンセル・値取得)はリクエストスコープのみに影響範囲がとどまるべきである。という点からです。
例えば、各リクエストで共有されるようなグローバルスコープの構造体に保持する事によって
意図せず別のリクエストによる副作用を受ける可能性があるので、そもそも保持しない事でそれを回避したほうが良い、という事です。