Golangのcontext.Valueの使い方
Go1.7でcontext
パッケージが標準パッケージに入りしいろいろなところで使われるようになってきた.先日リリースされたGo1.8においてもdatabase/sql
パッケージなどでcontext
のサポートが入るなどますます重要なパッケージになっている.
“Go1.7のcontextパッケージ”で書いたようにcontext
は「キャンセルのためのシグナルの受け渡しの標準的なインターフェース」として主に使われる.ある関数やメソッドの第1引数にcontext.Context
が渡せるようになっていればキャンセルを実行したときにその関数は適切に処理を中断しリソースを解放することを期待する.これはパッケージの作者とその利用者との間のある種の契約のようになっている(パッケージ側でgoroutine作るなというパターンもここで効いてくる).
これだけではなくcontext.Context
インターフェースにはValue
というメソッドも定義されている.これを使うと任意の値を受け渡すことができる(contextと言われるとこちらを想像する人も多い).これは便利だが注意して使わないと崩壊するのでどう使うべきかをまとめておく(contextも分かりやすい).
なぜ注意が必要か?
context.Value
のSetとGetは以下のように定義されている.
WithValue(parent Context, key, val interface{}) Context
Value(key interface{}) interface{}
WithValue
で値をセットしValue
で値を取り出す.注意するべきなのは型を見ればわかるようにtype-unsafeでコンパイラでチェックができないからである.要するにmap[interface{}]interface{}
である.つまり避けれるなら避けた方が良い.
例えばチームでAPIサーバーを開発していてあらゆる値が様々なHandlerで無防備にSetされたりGetされたりするようになると崩壊する.
どのようなときに使えるか?
ではどのようなときにValue
は有用になるか? ある特定のリクエストスコープ内で限定的な値を渡すのに便利に使える.例えば以下のようなものが考えられる.
- ユーザID
- 認証情報(Token)
- Distributed TraceのID
どのような値を渡すべきでないか?
あるいは適していないか.例えば,DB ClientやAPI Client,loggerなどである.これらはスコープに限定的ではないしそもそもテストがしにくくなる.これらはサーバーが依存として持つべきである.以下のようにmiddlewareで渡すかhandlerに持たせる(ジョブワーカーを書いている場合もStructを定義してそこに渡すべきである).
func MyMiddleware(db Database, next http.Handler) http.Handler {
return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
// Use db here.
next.ServeHTTP(w, r)
})
}
type MyHandler struct {
db Database
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {...}
使い方
context.Value
を使う上で注意するのは値へのアクセスを制限する,ちゃんと型を持たせることである.以下のやり方はどちらかというとstrictでパッケージ作者寄りのやりかただが,チームで何か書いている場合であってもむやみにいろいろな値がSetされてカオスになるよりは初めから厳しくやるのが良いと思う.
まずkeyは以下のようにunexportedな型をもったunexportedなconst
として定義する.こうしておけば意図しないところ(少なくともpackage外で)で値がSetされたりGetされることがなくなる.
type contextKey string
const tokenContextKey contextKey = "key"
値のSetとGetには以下のように専用の関数/メソッドを定義する(これらはpackage外にexportされても良い).少なくともGetは定義されているべきで関数内でtype assertionを実行し具体的な型として取り出せるようにする.
func SetToken(parents context.Context, t string) context.Context {
return context.WithValue(parents, tokenContextKey, t)
}
func GetToken(ctx context.Context) (string, error) {
v := ctx.Value(tokenContextKey)
token, ok := v.(string)
if !ok {
return "", fmt.Errorf("token not found")
}
return token, nil
}