Goブログ
Go並行処理パターン:コンテキスト
はじめに
Goサーバーでは、受信した各リクエストは独自のゴルーチンで処理されます。リクエストハンドラーは、データベースやRPCサービスなどのバックエンドにアクセスするために、追加のゴルーチンを開始することがよくあります。リクエストを処理するゴルーチンのセットは通常、エンドユーザーのID、認証トークン、リクエストの締め切りなど、リクエスト固有の値にアクセスする必要があります。リクエストがキャンセルされたり、タイムアウトしたりした場合、そのリクエストで動作しているすべてのゴルーチンは、システムがそれらが使用しているリソースを回収できるように、速やかに終了する必要があります。
Googleでは、リクエストスコープの値、キャンセル信号、締め切りをAPI境界を越えて、リクエストの処理に関与するすべてのゴルーチンに簡単に渡せるcontext
パッケージを開発しました。このパッケージは、contextとして公開されています。この記事では、このパッケージの使用方法について説明し、完全な実行可能な例を示します。
コンテキスト
context
パッケージのコアは、Context
型です。
// A Context carries a deadline, cancellation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. 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{} }
(この説明は要約されています。公式ドキュメントはgodocを参照してください。)
Done
メソッドは、Context
の代わりに実行されている関数へのキャンセル信号として機能するチャネルを返します。チャネルが閉じられると、関数は作業を中止して戻る必要があります。Err
メソッドは、Context
がキャンセルされた理由を示すエラーを返します。パイプラインとキャンセルの記事で、Done
チャネルのイディオムについて詳しく説明しています。
Context
には、Done
チャネルが受信専用であるのと同じ理由で、Cancel
メソッドがありません。キャンセル信号を受信する関数は通常、信号を送信する関数ではありません。特に、親オペレーションがサブオペレーションのためにゴルーチンを開始する場合、これらのサブオペレーションは親をキャンセルすることはできません。代わりに、WithCancel
関数(後述)は、新しいContext
値をキャンセルする方法を提供します。
Context
は、複数のゴルーチンによる同時使用に対して安全です。コードは、単一のContext
を任意の数のゴルーチンに渡し、そのContext
をキャンセルしてすべてに信号を送ることができます。
Deadline
メソッドを使用すると、関数は作業を開始すべきかどうかを判断できます。残り時間が少なすぎる場合、作業する価値がない可能性があります。コードは、I/O操作のタイムアウトを設定するために締め切りを使用することもできます。
Value
を使用すると、Context
にリクエストスコープのデータを渡すことができます。そのデータは、複数のゴルーチンによる同時使用に対して安全である必要があります。
派生コンテキスト
context
パッケージは、既存のContext
値から新しいContext
値を派生させる関数を提供します。これらの値はツリーを形成します。Context
がキャンセルされると、そこから派生したすべてのContext
もキャンセルされます。
Background
は、任意のContext
ツリーのルートです。キャンセルされることはありません
// Background returns an empty Context. It is never canceled, has no deadline, // and has no values. Background is typically used in main, init, and tests, // and as the top-level Context for incoming requests. func Background() Context
WithCancel
とWithTimeout
は、親のContext
よりも早くキャンセルできる、派生したContext
値を返します。受信リクエストに関連付けられたContext
は、通常、リクエストハンドラーが戻るときにキャンセルされます。WithCancel
は、複数のレプリカを使用する場合に冗長なリクエストをキャンセルするのにも役立ちます。WithTimeout
は、バックエンドサーバーへのリクエストに締め切りを設定するのに役立ちます。
// WithCancel returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed or cancel is called. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. type CancelFunc func() // WithTimeout returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed, cancel is called, or timeout elapses. The new // Context's Deadline is the sooner of now+timeout and the parent's deadline, if // any. If the timer is still running, the cancel function releases its // resources. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValue
は、リクエストスコープの値をContext
に関連付ける方法を提供します。
// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context
context
パッケージの使用方法を理解する最良の方法は、具体的な例を通して見ることです。
例:Googleウェブ検索
この例は、/search?q=golang&timeout=1s
のようなURLを処理するHTTPサーバーで、クエリ「golang」をGoogle Web Search APIに転送し、結果をレンダリングします。timeout
パラメーターは、その期間が経過した後にリクエストをキャンセルするようにサーバーに指示します。
コードは3つのパッケージに分割されています
- serverは、
main
関数と/search
のハンドラーを提供します。 - useripは、リクエストからユーザーIPアドレスを抽出し、
Context
に関連付ける関数を提供します。 - googleは、クエリをGoogleに送信するための
Search
関数を提供します。
サーバープログラム
serverプログラムは、golang
の最初のいくつかのGoogle検索結果を提供することで、/search?q=golang
のようなリクエストを処理します。/search
エンドポイントを処理するためにhandleSearch
を登録します。ハンドラーは、ctx
という初期のContext
を作成し、ハンドラーが戻るときにキャンセルされるように手配します。リクエストにtimeout
URLパラメーターが含まれている場合、タイムアウトが経過するとContext
が自動的にキャンセルされます。
func handleSearch(w http.ResponseWriter, req *http.Request) { // ctx is the Context for this handler. Calling cancel closes the // ctx.Done channel, which is the cancellation signal for requests // started by this handler. var ( ctx context.Context cancel context.CancelFunc ) timeout, err := time.ParseDuration(req.FormValue("timeout")) if err == nil { // The request has a timeout, so create a context that is // canceled automatically when the timeout expires. ctx, cancel = context.WithTimeout(context.Background(), timeout) } else { ctx, cancel = context.WithCancel(context.Background()) } defer cancel() // Cancel ctx as soon as handleSearch returns.
ハンドラーは、リクエストからクエリを抽出し、userip
パッケージを呼び出してクライアントのIPアドレスを抽出します。クライアントのIPアドレスはバックエンドリクエストに必要であるため、handleSearch
はそれをctx
に付加します。
// Check the search query. query := req.FormValue("q") if query == "" { http.Error(w, "no query", http.StatusBadRequest) return } // Store the user IP in ctx for use by code in other packages. userIP, err := userip.FromRequest(req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } ctx = userip.NewContext(ctx, userIP)
ハンドラーは、ctx
とquery
を使用してgoogle.Search
を呼び出します。
// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
検索が成功した場合、ハンドラーは結果をレンダリングします。
if err := resultsTemplate.Execute(w, struct { Results google.Results Timeout, Elapsed time.Duration }{ Results: results, Timeout: timeout, Elapsed: elapsed, }); err != nil { log.Print(err) return }
パッケージ userip
useripパッケージは、リクエストからユーザーIPアドレスを抽出し、Context
に関連付けるための関数を提供します。Context
は、キーと値の両方がinterface{}
型であるキー値マッピングを提供します。キーの型は等価性をサポートする必要があり、値は複数のゴルーチンによる同時使用に対して安全である必要があります。userip
のようなパッケージは、このマッピングの詳細を隠し、特定のContext
値への厳密に型付けされたアクセスを提供します。
キーの衝突を避けるために、userip
はエクスポートされていない型key
を定義し、この型の値をコンテキストキーとして使用します。
// The key type is unexported to prevent collisions with context keys defined in // other packages. type key int // userIPkey is the context key for the user IP address. Its value of zero is // arbitrary. If this package defined other context keys, they would have // different integer values. const userIPKey key = 0
FromRequest
は、http.Request
からuserIP
値を抽出します。
func FromRequest(req *http.Request) (net.IP, error) { ip, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) }
NewContext
は、提供されたuserIP
値を保持する新しいContext
を返します。
func NewContext(ctx context.Context, userIP net.IP) context.Context { return context.WithValue(ctx, userIPKey, userIP) }
FromContext
は、Context
からuserIP
を抽出します。
func FromContext(ctx context.Context) (net.IP, bool) { // ctx.Value returns nil if ctx has no value for the key; // the net.IP type assertion returns ok=false for nil. userIP, ok := ctx.Value(userIPKey).(net.IP) return userIP, ok }
パッケージ google
google.Search関数は、Google Web Search APIにHTTPリクエストを行い、JSONエンコードされた結果を解析します。リクエストが進行中にctx.Done
が閉じられた場合は、Context
パラメーターctx
を受け取り、すぐに戻ります。
Google Web Search APIリクエストには、検索クエリとユーザーIPがクエリパラメーターとして含まれています。
func Search(ctx context.Context, query string) (Results, error) { // Prepare the Google Search API request. req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) if err != nil { return nil, err } q := req.URL.Query() q.Set("q", query) // If ctx is carrying the user IP address, forward it to the server. // Google APIs use the user IP to distinguish server-initiated requests // from end-user requests. if userIP, ok := userip.FromContext(ctx); ok { q.Set("userip", userIP.String()) } req.URL.RawQuery = q.Encode()
Search
は、HTTPリクエストを発行し、リクエストまたはレスポンスが処理されている間にctx.Done
が閉じられた場合はそれをキャンセルする、ヘルパー関数httpDo
を使用します。Search
は、HTTPレスポンスを処理するためにクロージャーをhttpDo
に渡します。
var results Results err = httpDo(ctx, req, func(resp *http.Response, err error) error { if err != nil { return err } defer resp.Body.Close() // Parse the JSON search result. // https://developers.google.com/web-search/docs/#fonje var data struct { ResponseData struct { Results []struct { TitleNoFormatting string URL string } } } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return err } for _, res := range data.ResponseData.Results { results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL}) } return nil }) // httpDo waits for the closure we provided to return, so it's safe to // read results here. return results, err
httpDo
関数は、HTTPリクエストを実行し、新しいゴルーチンでレスポンスを処理します。ゴルーチンが終了する前にctx.Done
が閉じられた場合は、リクエストをキャンセルします。
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { // Run the HTTP request in a goroutine and pass the response to f. c := make(chan error, 1) req = req.WithContext(ctx) go func() { c <- f(http.DefaultClient.Do(req)) }() select { case <-ctx.Done(): <-c // Wait for f to return. return ctx.Err() case err := <-c: return err } }
コンテキストに対応するコードの適用
多くのサーバーフレームワークは、リクエストスコープの値を保持するためのパッケージと型を提供しています。既存のフレームワークを使用するコードとContext
パラメーターを予期するコードとの間にブリッジするために、Context
インターフェイスの新しい実装を定義できます。
たとえば、Gorillaのgithub.com/gorilla/contextパッケージを使用すると、ハンドラーはHTTPリクエストからキー値ペアへのマッピングを提供することで、受信したリクエストにデータを関連付けることができます。gorilla.goでは、Value
メソッドがGorillaパッケージの特定HTTPリクエストに関連付けられた値を返すContext
実装を提供します。
他のパッケージは、Context
と同様のキャンセルサポートを提供しています。たとえば、Tombは、Dying
チャネルを閉じることでキャンセルを通知するKill
メソッドを提供します。Tomb
は、sync.WaitGroup
と同様に、これらのゴルーチンが終了するのを待つためのメソッドも提供します。tomb.goでは、親のContext
がキャンセルされるか、提供されたTomb
がkillされたときにキャンセルされるContext
実装を提供します。
結論
Googleでは、Goプログラマーが、受信リクエストと送信リクエストの間の呼び出しパス上のすべての関数に、最初の引数としてContext
パラメーターを渡すことを要求しています。これにより、さまざまなチームによって開発されたGoコードがうまく相互運用できるようになります。タイムアウトとキャンセルを簡単に制御でき、セキュリティ認証情報のような重要な値がGoプログラムを適切に通過することを保証します。
Context
をベースに構築したいサーバーフレームワークは、パッケージとContext
パラメーターを予期するパッケージとの間でブリッジするために、Context
の実装を提供する必要があります。クライアントライブラリは、呼び出し元のコードからContext
を受け入れることになります。リクエストスコープのデータとキャンセルに共通のインターフェイスを確立することにより、Context
はパッケージ開発者がスケーラブルなサービスを作成するためのコードを共有することを容易にします。
さらに読む
次の記事:OSCONでのGo
前の記事:Goは2014年のOSCONに参加します
ブログインデックス