Go言語でAPI開発をしていて、一番多く使う標準パッケージと言っても過言ではないcontext。
その動作はなんとなく理解はしているものの、実際のコードを読んで深く理解したことがなかったため、今回は実際にコードやGoDocを読んで中身をまとめていこうと思います。
概要
contextは、Go1.7から標準パッケージに組み込まれています。
まずはGo公式ブログの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.
Goサーバーでは、サーバーに来たリクエストはそれぞれそれ自身のゴルーチンで処理されます。 リクエストハンドラーはよくデータベースやRPCサービスといったバックエンドにアクセスするために追加でゴルーチンを起動します。 リクエストの処理を行っているゴルーチンは、通常エンドユーザーのアイデンティティや認証トークン、リクエストの期限などリクエスト固有の値へのアクセス権が必要です。 リクエストがキャンセルされたりタイムアウトした場合には、システムがそれらのゴルーチンが使っていたリソースを再度要求することができるように、 そのリクエストの処理を行っていたすべてのゴルーチンは素早く終了すべきです。
つまりcontextパッケージが担う役割は別サーバへのリクエストのように時間のかかる処理をgoroutineで実行する場合に、処理に適切なTimeoutやDeadlineを設定して処理が停滞するのを防ぐ役割やCancel行うことで適切にネットワークのリソースを開放する役割を担います。
さらにはそのgoroutineは別のgoroutineを呼び出しそれがまた別の…と呼び出しの連鎖は深くなることが考えられます。その場合も親のTimeoutに合わせてその子は全て適切にキャンセルされリソースは解放されるべきです。
また、そのほかにも認証情報などをkvとして受け渡す機能も備わっています。キャンセルのためのシグナルをAPIの境界を超えて受け渡すための仕組みである.ある関数から別の関数へと,親から子へと,キャンセルを伝搬させることが可能になっていっます。
また、ContextはAPIの境界を越えて期限とキャンセルシグナルとリクエスト固有の値を保持します。
メソッドは複数のゴルーチンから同時に呼び出されても安全なものとなっています。
さらにはCancelのシグナルをAPI挟んで受け渡すことで、関数間でキャンセルを伝達することができるようになっています。
要するに
- Goのサーバーで入ってきた各リクエストは個別のgoroutineで処理される。
- リクエストのハンドラは、データベースやRPCのサービスにアクセスする。
- リクエスト上で動作するgoroutineはユーザー識別子や認証トークン、リクエストの期限みたいな特定のリクエストの値にアクセスする必要がある。
- リクエストがタイムアウトされた時、システムがそのリソースを再度利用できるように、すべての動作しているゴルーチンも素早く終了されるべきである。
Contextインターフェイス
ここはcontextパッケージで定義されているインターフェイスをみていきます。
golang.org/x/net/contextにある
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{}
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stored using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key interface{}) interface{}
}
}
ここでインターフェイスの各メソッドを見ていこうと思います。
//いつこのcontextがキャンセルされるかを返す。
Deadline() (deadline time.Time, ok bool)
//channelがクローズされていれば、その理由を返す。
Err() error
//コンテキストがキャンセルされたりタイムアウトした場合にcloseされ、channelを返す
Done() <-chan struct{}
//同コンテキストでに保存した値のkeyを指定するとvalueを返す
Value(key interface{}) interface{}
変数
//Canceled は,コンテキストがキャンセルされたときに Context.Err によって返されるエラー
var Canceled = errors.New("context canceled")
//DeadlineExceeded は,コンテキストの期限が過ぎたときに Context.Err によって返されるエラー
var DeadlineExceeded error = deadlineExceededError{}
type
CancelFunc は,作業を中止するよう指示します。 CancelFunc は,作業が停止するのを待ちません。 CancelFunc は,複数のゴルーチンから平行に呼び出すことができます。 最初の呼び出しの後, CancelFunc への後続の呼び出しは何もしません。
type CancelFunc func()
function
WithCancel ¶
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel は、新しい Done チャンネルを持つ親のコピーを返します。返されたコンテキストの Done チャンネルは、返された cancel 関数がコールされたとき、あるいは親コンテキストの Done チャンネルがクローズされたときのいずれか早いほうでクローズされます。
このコンテキストをキャンセルすると、コンテキストに関連付けられているリソースが解放されるので、このコンテキストで実行されている操作が完了したらすぐに cancel をコールするようにしましょう。
サンプルコード
WithDeadline ¶
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
親コンテキストのデッドラインがすでに d よりも早くなっている場合は、WithDeadline(parent, d) は意味的に parent と同等です。返されたコンテキストの Done チャンネルは、デッドラインの期限が切れたとき、 返されたキャンセル関数が呼び出されたとき、または親コンテキストの Done チャンネルが閉じられたときのいずれか早いほうで閉じられます。
このコンテキストをキャンセルすると、コンテキストに関連付けられているリソースが解放されるので、このコンテキストで実行されている操作が完了したらすぐに cancel をコールするようにしましょう。
指定した時間が経過ではなく指定した時刻になったらcancelが走ります。
サンプルコード
WithTimeout ¶
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout は WithDeadline(parent, time.Now().Add(timeout)) を返します。
timeoutの指定付きのWithCancel。第2引数で指定した時間が経過するとcancelが走ります。
サンプルコード
Background ¶
func Background() Context
Background は、null ではない空の Context を返します。キャンセルされることはなく、値もなく、期限もありません。これは通常、メイン関数、初期化、テストで使用され、 リクエストを受け取る際のトップレベルのコンテキストとして使用されます。
TODO ¶
func TODO() Context
TODO は、nullではないの空の Context を返します。どのコンテキストを使用するかが不明な場合や、まだ利用できない場合 (周囲の関数がまだ拡張されていないため、Context パラメータを受け付けることができません) には、コードは context.TODO を使用しなければなりません。
BackgroundとTODOの違いは?
機能的には何もありません。これらはビット単位で全く同じ値です。
違いは意図の違い。
context.TODO()で生成されるcontext.Contextは関数名のとおり、一時的なcontext.Contextになります。 GoDocにも「どのcontext.Contextを使うかわからないとき、他の関数が対応していなくてcontext.Contextが用意できないときに使ってね。」のように記載されている。
context.TODO() は、将来的にコンテキストを作成する必要がなくなることが予想される場合に使われることを意味している。
しかし、実際にはどちらも同じように動作します。
WithValue ¶
func WithValue(parent Context, key, val interface{}) Context
WithValue は、キーに関連付けられた値が val である親のコピーを返します。
context Values は、プロセスや API を通過するリクエスト・スコープされたデータにのみ使用し、パラメータを関数に渡す場合には使用しません。
提供されるキーは同等のものでなければなりません。また、コンテキストを使用するパッケージ間の衝突を避けるために、文字列型やその他の組み込み型であってはなりません。WithValue のユーザは、鍵の型を独自に定義しなければなりません。インターフェイス{}に代入する際の割り当てを避けるために、コンテキストキーはしばしば具象型の struct{} を持つことがあります。あるいは、エクスポートされたコンテキストキー変数の静的型はポインタかインターフェースでなければなりません。
サンプルコード
以上がGoのcontextパッケージの概要になります。
Go playgroundの簡単なコードも置いているので、ぜひ動かしてみてください。