goでreflectを使ってunexported fieldの値を見る

テストなどのhelperを作るときに、特定のstructのunexported fieldにアクセスしたくなることがある。 その方法のメモ(あとでまじめに書くかもしれない書かないかもしれない)。

with exported field

その前にreflect経由でのアクセスで考えてみる。以下の様なstructでとりあえずexported fieldにアクセスしてみる。

type S struct {
    Exported   int
    unexported int
}

reflect.ValueFieldByName()が使える。今回はintでやったが、通常利用するときは任意のstructということを考えてinterface{}として取り出す。

s := &S{Exported: 20, unexported: 10}
rv := reflect.ValueOf(s).Elem()

fmt.Println(rv.FieldByName("Exported").Interface())

これは上手く取り出せる。

20

with unexported field

同様のことをunexported fieldに対してやってみる。

s := &S{Exported: 20, unexported: 10}
rv := reflect.ValueOf(s).Elem()

fmt.Println(rv.FieldByName("unexported").Interface())

どうもpanicになるようだ。

panic: reflect.Value.Interface: cannot return value obtained from unexported field or method

unexported fieldなどへのアクセスは禁止されている。

with hack

いろいろ頑張ったところ、以下の様な感じでNewAt()を経由することで取り出せる。

s := &S{Exported: 20, unexported: 10}
rv := reflect.ValueOf(s).Elem()

rf := rv.FieldByName("unexported")
rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
fmt.Println(rf.Interface())

今度は大丈夫。

10

このhackが動く理由を調べるために、もう少し詳しく中を覗いてみることにする。

詳細

まず元々のpanicがどこで発生しているかと言うと、reflectパッケージのこの辺り。

value.go

func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}

func valueInterface(v Value, safe bool) interface{} {
    if v.flag == 0 {
        panic(&ValueError{"reflect.Value.Interface", Invalid})
    }
    if safe && v.flag&flagRO != 0 {
        // Do not allow access to unexported values via Interface,
        // because they might be pointers that should not be
        // writable or methods or function that should not be callable.
        panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
    }
    if v.flag&flagMethod != 0 {
        v = makeMethodValue("Interface", v)
    }
// ...

Value.Interface()valueInterface()safe=trueで実行していた。 そんなわけで、条件を迂回するには safe=false で実行するか v.flagがReadOnlyになっていなければ良い。

ここでflagの定義はこう。ちょっと分かりづらいので、aからeまでの番号を振って見た。

type flag uintptr

const (
    flagKindWidth        = 5 // there are 27 kinds
    flagKindMask    flag = 1<<flagKindWidth - 1
    flagStickyRO    flag = 1 << 5 // a
    flagEmbedRO     flag = 1 << 6 // b
    flagIndir       flag = 1 << 7 // c
    flagAddr        flag = 1 << 8 // d
    flagMethod      flag = 1 << 9 // e
    flagMethodShift      = 10
    flagRO          flag = flagStickyRO | flagEmbedRO
)

reflect.Valueの中を以下の様な形で無理やり覗いてみると、以下の様に変わっていた。たしかにNewAt()を経由することでflagStickyROが取り除かれている。

fmt.Printf("%b\n", reflect.ValueOf(rf).FieldByName("flag").Uint())

// 元の値
// dcba*****
// 110000010

// NewAt後の値
// dcba*****
// 110100010

NewAt()で新たなstructでwrapすることになる。そしてこのときに使われるreflect.Ptrというconstからflagが計算される。このPtrから作られたflagは先程の通りroではないから、Interface()が使える様になる。というわけ。

// NewAt returns a Value representing a pointer to a value of the
// specified type, using p as that pointer.
func NewAt(typ Type, p unsafe.Pointer) Value {
    fl := flag(Ptr)
    t := typ.(*rtype)
    return Value{t.ptrTo(), p, fl}
}

gist

参考