こんにちは! 京都開発本部テクニカルアーキテクトグループの櫻(@ysakura_)です。 クラウド会計Plusの性能問題の解決を担当しています。
先日Goの開発をしていて、 context.Context
に入れる値をリクエストスコープに限るべき理由をパッと説明できない事がありました。
そこで、自分なりの意見を纏めてみました。
はじめに
contextパッケージのコメントによると、Contextに入れる値はリクエストスコープなものに限るべきとされています。
// Use context Values only for request-scoped data that transits processes and // APIs, not for passing optional parameters to functions.
訳
contextの値は、プロセスやAPIを通過するリクエストスコープなデータに限って使ってください。 関数にオプショナルなパラメータを渡す目的で使うべきではありません。
Contextに入れる値を限定するべき背景
大きな理由として、ContextにおけるValueの扱いが難しい事が挙げられます。 Contextを使う必要がなければ、グローバル変数や構造体で値を持つ方が良いです。 扱いが難しい点に関して、以下の2点を取り上げます。
- Valueの型が
interface{}
である事 - 推奨されるValueへのアクセス方法
Valueの型がinterface{}
ContextにはKey-Valueの形式で値を入れる必要があります。
その際の型は、空のインターフェース(interface{}
)になります。
func WithValue(parent Context, key interface{}, val interface{}) Context
Value(key interface{}) interface{}
その為、具体的な型の情報にアクセスするには、interfaceを型に変換するType Assertionが必要になります。 以下の2つの観点で、扱いが難しいです。
- Type Assertionでは二つめの戻り値を取らない場合に変換が失敗するとpanicする
- Type Assertionのbool checkが入る分、コードが複雑になる
推奨されるValueへのアクセス方法
ここでもGoのコメントを元に話します。
// 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:
訳
KeyはContextの特定のValueを識別します。 ContextにValueを格納したい関数は、通常グローバル変数にKeyを割り当て、
context.WithValue
とContext.Value
の引数としてそのKeyを使います。 KeyにはEqualityをサポートする任意の型が使えます。 各パッケージで、衝突を避ける為にKeyを非公開な型として定義するべきです。ContextのKeyを定義するパッケージでは、格納されたValueに対するtype-safeなアクセサーを、そのKeyを使って提供すべきです。
contextパッケージのコメントでは以下の例が挙げられています。
Keyの衝突を避ける為に、unexporeted(非公開)な形で型を定義しています。
NewContext
, FromContext
の様に、Contextへの値の格納・取得を関数経由にする事が推奨されます。
この点で扱いが複雑になります。
// 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 }
リクエストスコープなデータがContextに向いている理由
Contextに入れる値は限定すべきという話をしたので、次はリクエストスコープなデータが向いている理由についてです。 これはContextがそうなる様に設計された、と自分は思います。 Goの標準パッケージを元にそれを紹介します。
Contextのライフサイクルがリクエストと同じ事
Goのnet/http
のRequest
構造体では以下の通り、context.Context
をフィールドに持ちます。
type Request struct { (中略) // ctx is either the client or server context. It should only // be modified via copying the whole Request using WithContext. // It is unexported to prevent people from using Context wrong // and mutating the contexts held by callers of the same request. ctx context.Context }
net/http
のサーバーでは、リクエストは個別のGoroutineで処理されます。このGoroutine内部で新規のContextがRequest
構造体に付与される為、ライフサイクルがリクエストと同じになります。
その為、リクエストスコープな値の管理には向いています。
一方で、ライフサイクルがリクエストと同じでないloggerなどはContextに入れるには適していません。
Contextの利用例
実際にリクエストスコープでContextのValueが使われている例を紹介します。
MiddlewareでContextに値を入れる
HTTPサーバーのmiddleware chainの例として、middleware内で何かしらの処理を行いその情報をContextに入れる事があります。
例) ユーザーの認証認可を行い、ユーザー情報をContextに詰めるMiddleware
package user import ( "context" "github.com/pkg/errors" ) type contextKey string const userKey contextKey = "user" // ContextWithUser ユーザー情報をコンテキストにセット func ContextWithUser(parent context.Context, user *User) context.Context { return context.WithValue(parent, userKey, user) } // UserFromContext ユーザー情報をコンテキストから取り出す func UserFromContext(ctx context.Context) (*User, error) { v := ctx.Value(userKey) user, ok := v.(*User) if !ok { return nil, errors.New("user not found") } return user, nil }
package middleware import ( "fmt" "net/http" "mymodule/user" ) // Auth 認可ミドルウェア func Auth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { authKey := r.Header.Get("Authorization") ctx := r.Context() u, err := user.Authorize(authKey) if err != nil { w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "UnAuthorized") return } ctx = user.ContextWithUser(ctx, u) next.ServeHTTP(w, r.WithContext(ctx)) } }
grpc/grpc-goのmetadata
gRPCのmetadataがリクエストのContextに含まれます。(詳細) Contextからmetadataを取得する関数も提供されています。
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) { md, ok := metadata.FromIncomingContext(ctx) // do something with metadata }
まとめ
context.Context
にはリクエストスコープな値のみを入れましょう。
リクエストスコープではない値は、グローバル変数や構造体に値を持たせるほうが良いでしょう。
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【サイトのご案内】 ■マネーフォワード採用サイト ■Wantedly ■京都開発拠点
【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』