はじめに
前回に引き続き、contextの用途から具体的な使い方までを自分用にまとめました。
前回の記事:初心者がGo言語のcontextを爆速で理解する ~ cancel編 ~
※ 勉強中、参考になる記事が多くあったので、記事の最後にURLを載せています。
コンテキストとは?
前回の記事「初心者がGo言語のcontextを爆速で理解する ~ cancel編 ~」で説明しています。
コンテキストの主な2つの用途
contextの主な用途は2つあります。
- Goroutineの適切なキャンセル
- リクエスト情報の伝搬
今回の記事では2つ目の「リクエスト情報の伝搬」にフォーカスします。
1つ目の「Goroutineの適切なキャンセル」は初心者がGo言語のcontextを爆速で理解する ~ cancel編 ~でまとめています。
リクエスト情報の伝搬
ざっくり使用イメージですが、リクエストが来た際にuserIDなどをcontextに保存し、後続の処理で使うときに使用します。
リクエスト情報の伝搬とありますが、どんなデータを伝搬するのか良いのでしょうか。
ここがこの記事で一番重要な箇所です。
最初にcontext valueのパッケージによると下記のような説明があります。
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
訳(Go言語による並行処理を参照)
コンテキスト値はプロセスやAPIの境界を通過するリクエストスコープでのデータに絞って使いましょう。
関数にオプションのパラメータを渡すために使うべきではありません。
なぜ関数にオプションのパラメータを渡すために使うべきではないのでしょうか、調べていきます。
まずcontextは複数のgoroutineから参照されます。
なので、返却されるvalueの値は複数のgoroutineからアクセスがあっても問題がない値にすべきです。
かみくだくと、イミュータブルな値でないとアクセスした際に予期しない挙動になる可能性があります。
つまり 1.contextに入れる値は不変であるイミュータブルな値にする必要があります。
次に、context valueから取得した値を使って関数を呼び出したり、振る舞いが変わってしまう場合、
それは関数にオプションのパラメータを渡していると言えます。
なので、2.contextに入れる値はその値によって振る舞いが変わるものは入れてはいけません。
ここでcontext valueに一度は入れたくなるであろう値を考えてみます。
どれもリクエストを通して、後続の処理で共通して使いたくなる値です。
- ユーザーID
- リクエストID
- 認証情報
- リクエストトークン
- URL
- Logger
- DBの接続情報
先に確認した条件からみてみると、、
1.contextに入れる値は不変であるイミュータブルな値のみにする必要があります。
2.contextに入れる値はその値によって振る舞いが変わるものは入れてはいけません。
とあるので、
下の4つがcontext valueに格納して良い値になるのだと思います。
- ユーザーID
- リクエストID
- 認証情報
- リクエストトークン
LoggerやDBの接続情報はサーバ内部の処理なので、構造体に定義して使うか、
共通して使う処理ならが、middlewareなどに持たせるのが良いみたいです。
※ ここら辺間違ってる可能性もあります、正確なことがわかる方教えてください。。
コンテキスト value の使い方ざっくり
contextを読んだ方が早いですがざっくりとした説明をします。
Contextの型
type Context interface {
// ・・・ 今回説明しない箇所は省略
// Valueはkeyに紐付いた値を返し、設定した値がない場合はnilを返します。
Value(key interface{}) interface{}
}
Context.Value(key)でkeyに紐づく値を返します。また、返ってくる値がinterfaceの型なので注意が必要です。
マッチするkeyが存在しないときはnilを返却します。
Function
func WithValue(parent Context, key interface{}, val interface{}) Context
setのイメージです。
第一引数に、key-valueを格納するcontextを指定し、key-valueをセットします。
そしてセット済みのcontextが返却されます。
具体的なコードベース
まず簡単に単純な使い方をみていきます。
func main() {
ctx := context.Background()
ctxValue1 := context.WithValue(ctx,"hoge",1)
ctxValue2 := context.WithValue(ctxValue1,"piyo",2)
ctxValue3 := context.WithValue(ctxValue2,"fuga",3)
ctxValue4 := context.WithValue(ctxValue3,"fuga",4) // fugaを上書き
go sayValue(ctxValue4)
time.Sleep( 2 * time.Second)
}
func sayValue(ctx context.Context) {
for {
fmt.Print("hoge",ctx.Value("hoge")," : ")
fmt.Print("piyo",ctx.Value("piyo")," : ")
fmt.Println("fuga",ctx.Value("fuga"))
time.Sleep(1 * time.Second)
}
}
// hoge1 : piyo2 : fuga 4
// hoge1 : piyo2 : fuga 4
コードの説明としては
WithValue()でkey-valueをセットした新たなcontextを生成しています。
fugaのみ子のcontextで上書きをしています。(子で上書いた値が優先されていることがわかります。)
context valueの問題
- 返却される値はインタフェースなので、キャストする必要がある。
- 型を区別できないので、コンパイル時にバグを発見することができない。
- goroutine同士がcontext valueにセットした情報を知る必要がある
ここら辺の問題を払拭した型安全な使い方を見ていきます。
型安全な使い方(推奨)
func main() {
ctx := context.Background()
ctx1 := SetAuthToken(ctx, "auth-token-value")
authToken, ok := GetAuthToken(ctx1)
if !ok {
fmt.Println("failed")
return
}
fmt.Println("success : ", authToken)
}
// keyの型情報を定義
type contextKey string
// ここにkeyを設定していく
var (
keyAuthToken contextKey = "auth-token"
// keyUserID contextKey = "user-id"
)
// setするためのfunction
func SetAuthToken(ctx context.Context, value string) context.Context {
return context.WithValue(ctx, keyAuthToken, value)
}
// getするためのfunction
func GetAuthToken(ctx context.Context) (string, bool) {
val, ok := ctx.Value(keyAuthToken).(string)
return val, ok
}
共通のkeyを定義しget/setの中に隠蔽することで、使う側はキャスト変換などを意識せずに使うことができる。
keyにひもづくデータが取得できなかった場合は、bool値にfalseが返却される
最後に
contextの用途や具体的な使い方を学ぶことができました。
これから案件先でcontextを使った実装をやっていくと思うので、この段階で勉強しておいてよかったです。
「Goroutineの適切なキャンセル」については初心者がGo言語のcontextを爆速で理解する ~cancel編 ~にまとめました。
参考資料
・Goの並行パターン:コンテキスト (Go Concurrency Pattern: Context)
・Go1.7のcontextパッケージ
・golangでcontextパッケージを使う
・Go言語による並行処理
・Context keys in Go
・Globally unique key for context value in Golang
・Context isn’t for cancellation
・Golangのcontext.Valueの使い方