goでreflectを使ってunexported fieldの値を見る
テストなどのhelperを作るときに、特定のstructのunexported fieldにアクセスしたくなることがある。 その方法のメモ(あとでまじめに書くかもしれない書かないかもしれない)。
with exported field
その前にreflect経由でのアクセスで考えてみる。以下の様なstructでとりあえずexported fieldにアクセスしてみる。
type S struct { Exported int unexported int }
reflect.Value
のFieldByName()
が使える。今回は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} }