7月29日付けのgolang.orgブログエントリーで context というパッケージが紹介されました。
Go Concurrency Patterns: Context - The Go Blog
参考: Go言語のcontextパッケージについてのやりとり - ワザノバ | wazanova
今現在、業務でGo言語を使ったWebサービスを書いているのですが、contextはリクエストのキャンセルとタイムアウトとリクエストスコープの変数を扱う、Google社内で利用が標準化されているパッケージだという事なので、エントリーを翻訳しました。
以下和訳。
Introduction
Goサーバーでは、入ってくる各リクエストは専用のgoroutineで処理されます。リクエストハンドラは、データベースやRPCサーバーにアクセスするためにしばしば追加のゴルーチンを開始します。リクエスト上のゴルーチンは、一般的にエンドユーザーのアイデンティティ、認証トークン、リクエストのデッドラインなどのリクエスト特有の情報にアクセスする必要があります。リクエストがキャンセルされるかタイムアウトした場合、システムが資源を再利用できるようにするため、そのリクエスト上で動作していた全てのゴルーチンは即座に終了すべきです。
Googleで私たちは、リクエストスコープの値、キャンセレーションシグナル、そしてAPI境界をまたぐデッドラインを、リクエスト処理に関わる全てのゴルーチンに渡す事を簡単にするためのcontextパッケージを開発しました。パッケージは code.google.com/p/go.net/contextで公開されています。この記事では、パッケージの使い方と完全な動作例を紹介します。
Context
contextパッケージの核となるのはContext型です:
type Context interface {
Done() <-chan struct{}
Err() error
Deadline() (deadline time.Time, ok bool)
Value(key interface{}) interface{}
}
(この記述は要約です。godocが信頼できます)
DoneメソッドはContextを実行中の関数へのキャンセレーションシグナルとして振る舞うチャンネルを返します:そのチャンネルがクローズされたら、関数は自分たちの仕事を放棄してreturnするべきです。ErrメソッドはそのContextがキャンセルされた理由を示すerrorを返します。Pipelines and CancelationでDoneチャンネルイディオムについてより詳しく議論しています。
Contextには、Doneチャンネルがreceive-onlyであることと同じ理由によりCancelメソッドがありません:キャンセレーションシグナルを受信している関数は、通常そのシグナルを送信する関数と同じではないからです。特に、親オペレーションがサブオペレーションのためにゴルーチンを開始したとき、それらのサブオペレーションは親をキャンセルできるべきではありません。そのかわりに、WithCancel関数(後述)は新しいContext値をキャンセルする方法を提供します。
Contextは複数ゴルーチンによる同時利用に対して安全です。コードは単一のContextをいくつのゴルーチンにでも渡せますし、それら全てにシグナルしてそのContextをキャンセルする事も出来ます。
Deadlineメソッドにより、関数は自分が仕事を開始するべきかどうか判断することができます;もし残り時間がほとんどなければ、全くの無駄になるかもしれません。コードはI/Oオペレーションのタイムアウトを設定するのに使う事も出来ます。
Valueにより、Contextでリクエストスコープなデータを運ぶ事が出来ます。そのデータは複数ゴルーチンによる同時利用に対して安全である必要があります。
派生コンテキスト
contextパッケージは既存のContextから新しいContextを派生するための関数を提供します。これらの値はツリーを形成します; あるContextがキャンセルされたとき、そこから派生している全てのContextもまたキャンセルされます。
Backgroundは全てのContextツリーのルートです;決してcancelされることはありません。
func Background() Context
WithCancelとWithTimeoutは、親Contextがキャンセルされるよりも早くキャンセルできる派生Contextの値を返します。到着したリクエストに関連づけられたContextは、一般的にはリクエストハンドラがreturnした時にキャンセルされます。WithCancelは複数レプリカ使用時に冗長なリクエストをキャンセルするのにも便利です。WithTimeoutは、バックエンドサーバーへのリクエストにデッドラインを設定するのに便利です:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
type CancelFunc func()
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValueはrequest-scopedな値をContextに関連づける方法を提供します
func WithValue(parent Context, key interface{}, val interface{}) Context
contextパッケージの使い方を見る一番の方法は、動いているサンプルを見る事です。
Example: Google Web Search
この例は、/search?q=golang&timeout=1s のようなURLを、クエリ"golang"をGoogle Web Search APIにフォワードして結果を表示するという風にハンドルするHTTPサーバーです。timeoutパラメーターはサーバーにこの時間が経過した後にリクエストをキャンセルするように指示します。このコードは三つの部分に分かれています:
The server program
serverプログラムは/search?q=golang のようなリクエストに対して最初のいくつかのgolangのGoogle検索結果を返します。ハンドラはctxと呼ばれる初期Contextを作成し、ハンドラがreturnした時にキャンセルされるように準備します。リクエストがtimeout URLパラメータを含んでいたら、Contextはtimeout経過後に自動的にキャンセルされます:
func handleSearch(w http.ResponseWriter, req *http.Request) {
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel()
ハンドラはリクエストからクエリを抜き出し、useripパッケージを呼び出す事でクライアントのIPアドレスを取得します。クライアントのIPアドレスはバックエンドリクエストに必要なので、handleSearchはそれをctxにくっつけます:
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}
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を呼び出します:
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
}
Package userip
useripパッケージはリクエストからユーザーIPアドレスを抜き出してContextに関連づける関数を提供します。Contextはkey-valueマッピングを提供しますが、そのkeyとvalueはどちらもinterface{}型です。Key型は等価をサポートし、valueは複数ゴルーチンからの同時利用に対して安全である必要があります。useripのようなパッケージはこのマッピングの詳細を隠蔽し、特定のContext値への強く型付けされたアクセスを提供します。
キー衝突を避けるために、useripはexportされない型のkeyを定義してcontext keyとしてこの型の値を使います。
type key int
const userIPKey key = 0
FromRequestはuserIP値をhttp.Requestから抜き出します
func FromRequest(req *http.Request) (net.IP, error) {
s := strings.SplitN(req.RemoteAddr, ":", 2)
userIP := net.ParseIP(s[0])
if userIP == 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はuserIPをContextから抜き出します:
func FromContext(ctx context.Context) (net.IP, bool) {
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
google.Search関数はGoogle Web Search APIへのHTTPリクエストを作ってJSONエンコードされた結果をパースします。これはContextパラメータctxを受け入れ、もしリクエストを飛ばしている間にctx.Doneがクローズされたらすぐにリターンします。
Google Web Search APIリクエストはサーチクエリとユーザーIPをクエリパラメータに含みます:
func Search(ctx context.Context, query string) (Results, error) {
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 userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
Searchはヘルパー関数httpDoを使ってHTTPリクエストを発行し、もしリクエストかレスポンスが処理中にctx.Doneがクローズされたらキャンセルします。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()
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
})
return results, err
httpDo関数は新しいゴルーチンの中でHTTPリクエストを実行してレスポンスを処理します。もしゴルーチンがexitする前にctx.Doneがクローズしたらキャンセルします。
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
tr := &http.Transport{}
client := &http.Client{Transport: tr}
c := make(chan error, 1)
go func() { c <- f(client.Do(req)) }()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c
return ctx.Err()
case err := <-c:
return err
}
}
コードをContextに適合する
多くのサーバーフレームワークがリクエストスコープな値を運ぶためのパッケージと型を提供しています。私たちは、既存のフレームワークとContextパラメータを期待するコードとの間をブリッジする新しいContextインターフェイスの実装を定義する事が出来ます。
例えば、Gorillaのgithub.com/gorilla/contextパッケージは、HTTPリクエストからkey-valueペアへのマップを提供する事でハンドラにデータをリクエストに関連づける事を許します。gorilla.goにおいて、私たちはGorillaパッケージ内で特定のHTTPリクエストに関連づけられた値を返すValueメソッドを持つContext実装を提供します。
他のパッケージは、Contextに似たキャンセレーションサポートをサポートしてきました。例えば、TombはDyingチャンネルをcloseすることでキャンセルをシグナルするKillメソッドを提供します。Tomはまたこれらのゴルーチンが終了するのを待つ、sync.WaitGroupに似たメソッドも提供します。tomb.goでは、私たちは親のContextがキャンセルされるか与えられたTombがkillされるかの場合にキャンセルされるContext実装を提供します。
結論
Googleでは、私たちはGoプログラマーにContextパラメーターをリクエストの到着から返却までの間に呼ばれる全ての関数の第一引数にContextパラメーターを渡す事を要求しています。これは、たくさんの異なるチームで開発されたGoのコードに良好な相互運用性をもたらします。タイムアウトとキャンセレーションのシンプルな制御を導入し、セキュリティクレデンシャルのようなクリティカルな値が適切にGoプログラム内を移動する事を確かなものにします。
Context上に構築したいサーバーフレームワークは、それらのパッケージとContextパラメータを期待するものの間をブリッジするContext実装を提供するべきです。そうすればそれらのクライアントライブラリは呼び出しコードからContextを受け入れることでしょう。リクエストスコープなデータとキャンセレーションのための共通インターフェイスを確立する事により、Contextはパッケージ開発者がスケーラブルなサービスを作るためのコードを共有するのを簡単にするのです。
By Sameer Ajmani