LoginSignup
22
21

More than 5 years have passed since last update.

context.Contextでリクエストスコープな値を持ち回す

Posted at

WEBアプリを作る際、ログインユーザー情報など、リクエスト毎に値を保持しておいて各層のコンポーネントから透過的にアクセスしたいケースがあります。ここではリクエストスコープと呼んでおきます。

JavaのWEBフレームワークなどではリクエストスコープはthread local storageを使って実装されていたりします1が、Go言語にはそれに相当するものがありません。
そもそもGoはgoroutineで手軽に並行処理を書けるのが大きなウリなので、thread local storageではその魅力が生かせなくなります(異なるgoroutine間で値を共有出来ません)。
その為Goではリクエストスコープな値が必要な場合、必ず引数で持ち回さなければなりません。

ただ、必要なものを全て別々の引数で受け渡していると引数の数が増えてコードの見通しが悪くなりますよね。

Go1.7からcontextパッケージが追加されました。context.Contextを各層コンポーネント関数の引数に受け渡すルールにしておけば、その中にあるデータに透過的にアクセスできる様になります。
※context.Contextの機能はこれだけではありませんが、本記事では割愛します。

以降、ログインユーザー情報の設定と取得の例を使い方を記述します。

値の設定

リクエストに紐づくログインユーザー情報を設定しておきます。

実際のコードではログインが必要なAPIの前処理でリクエストヘッダのtokenなどからユーザーIDを取得し設定することになると思います。

type userKey struct{}
    c := context.Background() // WAFが用意しているcontextがあればそれを使う
    c = context.WithValue(c, userKey{}, userID)

context.WithValue関数を使って値を設定した新しいcontextを作成します。

第2引数のkeyはinterface{}なので等値比較出来るものなら何でも渡せますが、string等基本型を使用すると他のライブラリとkeyが競合する危険性があります。上記の様にstructのalias typeをkeyにしておくと、競合がおきません。2

値の取得

userId := c.Value(userKey{}).(string)

context.ContextValueメソッドで値を取得します。戻り値はinterface{}なのでtype assertionする必要があります。
下記の様な関数を用意しておけば利用する側がtype assertionする必要はなくなりますね。

func UserID(c context.Context) string {
    return c.Value(userKey{}).(string)
}

まとめ

ログインユーザー情報に限らず、トランザクション情報や、リクエストスコープキャッシュなど、いろいろと使えると思います。

他言語のthread local storageに比べると記述のシンプルさは劣るかと思いますが、Concurrent Programmingを考慮するとこういう方式になるのも仕方ないかなーと思っています。

上記コード例、もっと良いパターンなどあれば是非ご指摘くださいm(_ _)m


  1. 他の言語は詳しくありませんが同様なものが多いと推測します 

  2. 最新のgolintではkeyにalias typeを使わないとshould not use basic type xxxx as key in context.WithValueと怒られます。 

22
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
21