はじめに
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
}
結論からいいます。上記コードをみればわかるように衝突しません。
key
やkey2
はそれぞれ他のキーに影響を受けずに変更、取得できています。
一方、誤ってかかれたkey3
への変更はkey3``key4
両方に影響しています。
原因
以下の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)
}
このことから、
key
とkey3
を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:
- x's type is identical to T.
- x's type V and T have identical underlying types and at least one of V or T is not a defined type.
key
とkey2
は比較不可能である理由は、key
とkey2
ともにdefined type
であるからであるとわかります。
でも、key
と key3
は比較はできてしまいます。なぜなら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.
なので、key
とkey3
は比較可能であるだけでなく、イコールになることがわかります。
ただ、実は上記の話は本筋から脱線してます。なぜなら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.
とあるように他の型と型同一になることがありません。
contextKey
、contextKey2
、struct{}
ともに異なる型となります。
そのため、ikey
とikey2
、ikey
とikey3
ともにイコールにならないことがわかります。
以上です。
雑感
個人的には、key
とkey3
を比較するとイコールなのに、それらをinterface{}
でキャストして比較した途端にイコールにならないというのは自明ではないので新たな発見でした。