🍊

【Go】contextパッケージでkeyの重複を回避する方法

2023/04/10に公開

はじめに

Go言語のcontextパッケージを使用する際に、値を共有するために使われるValue型のkeyは、interface{}型であることが特徴的です。

context.WithValue()関数

context.WithValue()関数は、キーと値のペアをcontextに設定するために使用されます。この関数は、以下のように定義されています。

func WithValue(parent Context, key, val interface{}) Context

keyには、どのような型でも指定できます。一方、valには、interface{}型を満たす任意の型の値を指定することができます。
context.WithValue には以下のコメントがあります。

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// 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.

keyの衝突

contextパッケージのValue型のkeyは、値を共有するために使われますが、異なるパッケージ間で同じ名前のkeyを使用すると、値が上書きされてしまうことがあります。

例えば、以下のようにpkg1パッケージとpkg2パッケージで同じ名前のkeyを使用した場合、値が上書きされてしまいます。

package pkg1

import "context"

func Foo(ctx context.Context) {
    ctx = context.WithValue(ctx, "key", "value1")
}

package pkg2

import "context"

func Bar(ctx context.Context) {
    ctx = context.WithValue(ctx, "key", "value2")
}

上記のように、異なるパッケージで同じ名前のkeyを使用すると、value2で上書きされてしまいます。このような問題を避けるためには、各パッケージで独自のkeyを使用することが重要です。

解決策1:非公開key型を定義する

非公開のkey型を定義することで、各パッケージで独自のkeyを使用することができます。以下は、pkg1パッケージとpkg2パッケージで異なる型のkeyを使用する例です。

package pkg1

import "context"

type key int

const (
	a key = iota
)

func Foo(ctx context.Context) {
    ctx = context.WithValue(ctx, a, "value1")
}

package pkg2

import "context"

type key int

const (
	a key = iota
)

func Bar(ctx context.Context) {
    ctx = context.WithValue(ctx, a, "value2")
}

上記のように、pkg1, pkg2パッケージ共にkey型という非公開型を定義し、それぞれkey型の定数aをkeyとしてcontextに値を付与しています。

解決策2:構造体型のゼロ値を使う

構造体型のゼロ値を使うことで、各パッケージで独自のkeyを使用することができます。以下は、pkg1パッケージとpkg2パッケージで異なる構造体型を使用する例です。

package pkg1

import "context"

type key1 struct{}

func Foo(ctx context.Context) {
    ctx = context.WithValue(ctx, key1{}, "value1")
}

package pkg2

import "context"

type key2 struct{}

func Bar(ctx context.Context) {
    ctx = context.WithValue(ctx, key2{}, "value2")
}

上記のように、key1とkey2という異なる構造体型を定義し、各パッケージで独自のkeyを使用することができます。

まとめ

Go言語のcontextパッケージを使用する際に、Value型のkeyには、どのような型でも指定できますが、異なるパッケージで同じ名前のkeyを使用すると、値が上書きされることがあります。この問題を回避するために、各パッケージで独自のkeyを使用する方法として、非公開key型を定義する方法と、構造体型のゼロ値を使う方法があります。どちらの方法でも、各パッケージで異なるkeyを使用することができます。適切にkeyを定義することで、contextパッケージを効果的に利用することができます。

Discussion