web-dev-qa-db-ja.com

Golang context.WithValue:複数のキーと値のペアを追加する方法

Goのcontextパッケージを使用すると、リクエスト固有のデータをリクエスト処理関数のスタックに渡すことができます。

_func WithValue(parent Context, key, val interface{}) Context
_

これは新しいContextを作成します。これは親のコピーであり、キーでアクセスできる値valが含まれています。

Contextに複数のキーと値のペアを保存する場合、どうすればよいですか?最後のWithValue()の呼び出しから受け取ったContextを渡すたびに、WithValue()を数回呼び出しますか?これは面倒です。
または、構造体を使用して、すべてのデータをそこに配置します。 1つの値(構造体)のみを渡す必要があり、そこから他のすべてにアクセスできますか?

または、WithValue()に複数のキーと値のペアを渡す方法はありますか?

22
alex

選択肢のほとんどをリストアップしました。求める答えは、コンテキストに保存されている値をどのように使用するかによって異なります。

context.Context は不変オブジェクトです。キーと値のペアで「拡張」するには、そのコピーを作成し、新しいキーと値をコピーに追加するだけです(これは完了です)ボンネットの下、 context パッケージ)。

さらにハンドラーが透過的にキーですべての値にアクセスできるようにしたいですか?次に、最後の操作のコンテキストを常に使用して、すべてをループに追加します。

ここで注意すべきことの1つは、context.Contextがキーと値のペアを格納するためにフードの下でmapを使用しないことです。これは最初は驚くかもしれませんが、同時に使用しても安全です。

mapを使用する

たとえば、多くのキーと値のペアがあり、キーfastで値をルックアップする必要がある場合、それぞれを個別に追加するとContextになり、そのValue()メソッドは遅くなります。この場合、Context.Value()を介してアクセスできる単一のmap値としてすべてのキーと値のペアを追加し、その中の各値をO(1) timeの関連付けられたキーでクエリすることができます。ただし、マップはコンカレントゴルーチンから変更される可能性があるため、これはコンカレント使用に対して安全ではないことを理解してください。

structを使用する

追加するすべてのキーと値のペア用のフィールドを持つ大きなstruct値を使用する場合、これも実行可能なオプションです。 Context.Value()を使用してこの構造体にアクセスすると、構造体のコピーが返されるため、同時に使用しても安全です(各ゴルーチンは異なるコピーしか取得できません)が、多くのキーと値のペアがある場合、これは不要なコピーになります誰かがそこから単一のフィールドを必要とするたびに大きな構造体の。

ハイブリッドソリューションを使用する

hybrid解決策は、すべてのキーと値のペアをmapに入れ、このマップのラッパー構造体を作成し、map(エクスポートされていないフィールド)を非表示にすることです。マップに格納されている値のゲッターのみを提供します。このラッパーのみをコンテキストに追加すると、複数のゴルーチンに対して安全な同時アクセスを保持します(mapはエクスポートされません)、まだビッグデータをコピーする必要はありませんmap値はキー値データのない小さな記述子です)、それでもfastになります(最終的にはマップにインデックスを付けます)。

これは次のようなものです。

type Values struct {
    m map[string]string
}

func (v Values) Get(key string) string {
    return v.m[key]
}

それを使用して:

v := Values{map[string]string{
    "1": "one",
    "2": "two",
}}

c := context.Background()
c2 := context.WithValue(c, "myvalues", v)

fmt.Println(c2.Value("myvalues").(Values).Get("2"))

出力( Go Playground で試してください):

two

パフォーマンスが重要でない場合(または、キーと値のペアが比較的少ない場合)、それぞれを個別に追加します。

25
icza

はい、あなたは正しいです。毎回結果を渡してWithValue()を呼び出す必要があります。なぜこのように機能するのかを理解するには、コンテキストの背後にある理論について少し考える価値があります。

コンテキストは、実際にはコンテキストツリー内のノードです(したがって、さまざまなコンテキストコンストラクターが「親」コンテキストを取得します)。コンテキストから値を要求すると、実際には、問題のコンテキストからツリーを検索するときに、キーに一致する最初の値が要求されます。これは、ツリーに複数のブランチがある場合、またはブランチのより高いポイントから開始する場合、異なる値を見つけることができることを意味します。これは、コンテキストの力の一部です。一方、キャンセル信号は、キャンセルされたもののすべての子要素にツリーを伝播するため、単一ブランチをキャンセルしたり、ツリー全体をキャンセルしたりできます。

例として、コンテキストに保存できるさまざまなものを含むコンテキストツリーを次に示します。

Tree representation of context

黒のエッジはデータルックアップを表し、灰色のエッジはキャンセル信号を表します。それらは反対方向に伝播することに注意してください。

キーを格納するためにマップまたは他の構造を使用する場合、コンテキストのポイントを壊すことになります。リクエストの一部のみをキャンセルすることはできなくなります。リクエストのどの部分にあったかなどに応じて、ログが記録される場所を変更します。

TL; DR —はい、WithValueを数回呼び出します。

12
Sam Whited

「icza」が言ったように、値を1つの構造体にグループ化できます。

type vars struct {
    lock    sync.Mutex
    db      *sql.DB
}

次に、この構造体をコンテキストに追加できます。

ctx := context.WithValue(context.Background(), "values", vars{lock: mylock, db: mydb})

そして、あなたはそれを取得することができます:

ctxVars, ok := r.Context().Value("values").(vars)
if !ok {
    log.Println(err)
    return err
}
db := ctxVars.db
lock := ctxVars.lock
1
omotto