LoginSignup
7
2

More than 3 years have passed since last update.

GoのcontextのValueのkeyの型を再考する

Last updated at Posted at 2020-05-17

はじめに

GoのContextパッケージではctx = WithValue(ctx, key ,value)でcontextに紐付けた値をctx.Value(key)で取得します。

このときkeyとしては他のコードとの衝突をさけるため、以下のコメントにあるようにtypeでオリジナルの型を定義するのが推薦されてます。

// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {

その際、struct{}で定義することが割と多いようで、例えば、DatadogのSpanKeyは以下のように定義されています。


type contextKey struct{}

var activeSpanKey = contextKey{}

// ContextWithSpan returns a copy of the given context which includes the span s.
func ContextWithSpan(ctx context.Context, s Span) context.Context {
    return context.WithValue(ctx, activeSpanKey, s)
}

では、仮にだれかが誤ってtypeで型宣言せずにKeyをstruct{}にて定義し、そのライブラリを利用したとき、他のライブラリ(例えばdatadogのspanなど)や自分が書いたコードのkeyとそのkeyは衝突しないのでしょうか?
と疑問に思ったのでその調査と原因を共有します。

確認

package main

import (
    "context"
    "fmt"
)

type contextKey struct{}
var key = contextKey{}

// 正しく書かれたライブラリを想定
type contextKey2 struct{}
var key2 = contextKey2{}

// 誤って書かれたライブラリを想定
var key3 = struct{}{}
// 誤って書かれたライブラリを想定
var key4 = struct{}{}

func main() {
    ctx := context.WithValue(context.Background(), key, "Go")
    fmt.Println(ctx.Value(key))  // Go
    fmt.Println(ctx.Value(key2)) // nil
    fmt.Println(ctx.Value(key3)) // nil
    fmt.Println(ctx.Value(key4)) // nil
    ctx = context.WithValue(ctx, key2, "Scala")
    fmt.Println(ctx.Value(key))  // Go
    fmt.Println(ctx.Value(key2)) // Scala
    fmt.Println(ctx.Value(key3)) // nil
    fmt.Println(ctx.Value(key4)) // nil
    ctx = context.WithValue(ctx, key, "Java")
    fmt.Println(ctx.Value(key))  // Java
    fmt.Println(ctx.Value(key2)) // Scala
    fmt.Println(ctx.Value(key3)) // nil
    fmt.Println(ctx.Value(key4)) // nil
    ctx = context.WithValue(ctx, key3, "Python")
    fmt.Println(ctx.Value(key))  // Java
    fmt.Println(ctx.Value(key2)) // Scala
    fmt.Println(ctx.Value(key3)) // Python
    fmt.Println(ctx.Value(key4)) // Python


    // 後ほど説明
    // fmt.Println(key == key2)  keyとkey2は比較不可能
    fmt.Println(key == key3) // true
    ikey := interface{}(key)
    ikey2 := interface{}(key2)
    ikey3 := interface{}(key3)

    fmt.Println(ikey == ikey2) // 比較可能になるが、false
    fmt.Println(ikey == ikey3) // false
}

結論からいいます。上記コードをみればわかるように衝突しません。
keykey2はそれぞれ他のキーに影響を受けずに変更、取得できています。
一方、誤ってかかれたkey3への変更はkey3key4両方に影響しています。

原因

以下のcontextのソースの抜粋です。Valueの実装を見ればわかりますが、引数のkeyと自分が保持しているkeyをinterface型として比較し、同一でない場合は自分が保持している親のContextに委譲していることがわかります。

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

このことから、
keykey3をinteface{}型として比較したときにイコールにならなければ衝突しないといえそうです。
その確認コードは上記のコードにすでにあり、イコールにならないのです。


fmt.Println(ikey == ikey3) // false

なぜ、イコールにならないか

もう一歩踏み込んでなぜikey == ikey3がfalseになるのかGolangのspecを確認しながらみていきましょう。

まず、==で比較可能なためには、代入可能性(Assignability)がないといけません。
このあたりのことは前回のGoの代入可能性についてちゃんと理解してみる
に詳しく書いてます。

一部代入可能性の条件を抜粋すると、

A value x is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:
1. x's type is identical to T.
2. x's type V and T have identical underlying types and at least one of V or T is not a defined type.

keykey2は比較不可能である理由は、keykey2ともにdefined typeであるからであるとわかります。

でも、keykey3は比較はできてしまいます。なぜならcontextKey型とstruct{}型はunderlying typeであるstruct{}が同じで、contextKey型はdefined typeでないからです。
そして、構造体の同一性は

Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.

なので、keykey3は比較可能であるだけでなく、イコールになることがわかります。

ただ、実は上記の話は本筋から脱線してます。なぜならValueのコードではinterface{}型同士を比較しているのでした。

interfaceの型同一性は以下でした。

Two interface types are identical if they have the same set of methods with the same names and identical function types. Non-exported method names from different packages are always different. The order of the methods is irrelevant.

当然、interface{}型なので、ikey,ikey2,ikey3ともに型同一で、型同一ならば比較可能です。(ここはちょっと蛇足感はありますが、interface型の型同一性の確認のため引用しました)

では、==でイコールになるのはどのような条件でしょうか。仕様に

Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

とあります。dynamic typesが同じである必要があるとあります。
typeで型を宣言したものは

The new type is called a defined type. It is different from any other type, including the type it is created from.

とあるように他の型と型同一になることがありません。
contextKeycontextKey2struct{}ともに異なる型となります。
そのため、ikeyikey2ikeyikey3ともにイコールにならないことがわかります。

以上です。

雑感

個人的には、keykey3を比較するとイコールなのに、それらをinterface{}でキャストして比較した途端にイコールにならないというのは自明ではないので新たな発見でした。

7
2
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
7
2