stop-the-world

takuya-a のブログ

Goで構造体の非公開フィールドにアクセスする方法

Go の 構造体 (struct) におけるフィールドは、フィールド名が小文字始まりであれば 非公開フィールド (unexported field) となり、パッケージ外からアクセスすることができません(参考: Exported identifiers - The Go Programming Language Specification)。 組織内で管理しているソースコードなら単に修正してしまえばよいのですが、外部のライブラリなどの場合、変更してもらうのは大変です。

このような、やむを得ない理由で非公開フィールドを参照したい場合、ちょっとした工夫が必要になります。 試してみたところ、以下の2つの方法で非公開フィールドを取り出すことができました。

  1. reflect.ValueOfunsafe.Pointer を使う方法
  2. go.mod でモジュールを replace する方法

1. reflect.ValueOfunsafe.Pointer を使う方法

以下のようにして reflect.ValueOfunsafe.Pointer を併用することで、非公開フィールドにアクセスすることができます。

このソースコードは、外部パッケージ outside の構造体 OuterStruct に定義された privateStruct などの非公開フィールドを取り出したい、という設定です。

同じく OuterStruct に定義された、 int 型の privateInt や、 string 型の privateString などの非公開フィールドの読み取り・書き込みもできました。

package main

import (
    "fmt"
    "reflect"
    "unsafe"

    "github.com/takuyaa/go-private-test/outside"
)

func main() {
    var outer *outside.OuterStruct = outside.NewOuterStruct()

    // var v reflect.Value = reflect.ValueOf(*outer) としてもよいが、コピーが走ってしまうため、もとの outer は変更できないし、効率も悪い
    var v reflect.Value = reflect.ValueOf(outer).Elem()
    fmt.Printf("v: %#v\n", v) // v: outside.OuterStruct{privateInt:10, privateString:"dog", privateStruct:(*outside.InnerStruct)(0xc0000b4030)}

    // privateStruct を取得
    var psv reflect.Value = v.FieldByName("privateStruct").Elem()
    fmt.Printf("psv: %#v\n", psv) // psv: outside.InnerStruct{Message:"Hi!"}
    // 直接は中身を取れない
    // is := sv.Interface() // panic: reflect.Value.Interface: cannot return value obtained from unexported field or method
    var is *outside.InnerStruct = (*outside.InnerStruct)(unsafe.Pointer(psv.UnsafeAddr())) // unsafe.Pointer を使うと中身を取れる
    fmt.Printf("is: %#v\n", is)                                                            // is: &outside.InnerStruct{Message:"Hi!"}

    // int 型の private フィールドの取得と変更
    var piv reflect.Value = v.FieldByName("privateInt")
    pi := (*int)(unsafe.Pointer(piv.UnsafeAddr()))
    fmt.Printf("pi: %#v\n", *pi) // pi: 10
    *pi = 111                    // フィールドの値を変更できる
    fmt.Printf("pi: %#v\n", *pi) // pi: 111

    // string 型の private フィールドの取得と変更
    var pstrv reflect.Value = v.FieldByName("privateString")
    ps := (*string)(unsafe.Pointer(pstrv.UnsafeAddr()))
    fmt.Printf("ps: %#v\n", *ps) // ps: "dog"
    *ps = "cat"                  // フィールドの値を変更できる
    fmt.Printf("ps: %#v\n", *ps) // ps: "cat"

    // PublicMethod の呼び出し
    pubm := reflect.ValueOf(outer).MethodByName("PublicMethod") // Elem() 経由ではなくポインタからアクセス
    fmt.Printf("pubm: %#v\n", pubm)                             // pubm: (func())(0x1082580)
    pubm.Call(nil)                                              // Hi!

    // privateMethod の呼び出しは失敗する
    prim := reflect.ValueOf(outer).MethodByName("privateMethod") // Elem() 経由ではなくポインタからアクセス
    fmt.Printf("prim: %#v\n", prim)                              // prim: <invalid reflect.Value>
    // prim.Call(nil)                                            // panic: reflect: call of reflect.Value.Call on zero Value
}
-- go.mod --
module github.com/takuyaa/go-private-test

go 1.13
-- outside/lib.go --
package outside

type InnerStruct struct {
    Message string
}

type OuterStruct struct {
    privateInt    int
    privateString string
    privateStruct *InnerStruct
}

func NewOuterStruct() *OuterStruct {
    return &OuterStruct{
        privateInt:    10,
        privateString: "dog",
        privateStruct: &InnerStruct{Message: "Hi!"},
    }
}

func (s *OuterStruct) privateMethod() {
    println(s.privateStruct.Message)
}

func (s *OuterStruct) PublicMethod() {
    println(s.privateStruct.Message)
}

上記のソースコードThe Go Playground にコピペするか、以下の URL でも試せます*1:

https://play.golang.org/p/iomnJeRjOns

Pros

  • 既存のコードには手を入れずに、最小限の追加実装で非公開フィールドにアクセス可能
    • 2, 3 行のコードで非公開フィールドへのポインタが取得できる

Cons

  • reflectunsafe を使うので、プロダクションのコードでは採用しづらい
    • パフォーマンス上の問題も起きうる
    • あくまでテストやデバッグで使うのに留めるのが無難そう
  • この方法では非公開メソッドへのアクセスはできない(後述)

非公開メソッドの呼び出しは難しい

上で見たように、非公開メソッドは MethodByNamereflect.Value を取得しても、 invalid reflect.Value となり、それを通して関数呼び出しをすることはできませんでした。

spance/go-callprivate もダメ元で試してみましたが、残念ながらうまくいきませんでした。((プライベートメソッドの場合、 reflect.Value を取得してもそれが invalid なので、 private.SetAccessible(method) が失敗します。))

alangpierce/go-forceexporttime.now のような、レシーバではない関数を呼び出すためのもののようで、用途が異なるようです(参考: how to call a unexported Func in a struct? · Issue #1 · alangpierce/go-forceexport)。

2. go.mod でモジュールを replace する方法

2 つ目の方法は、 Go Modulesreplace の機能を使う方法です*2

  1. 対象となる外部モジュールのソースコードを取得し、利用側のリポジトリに置く
    • 以下では、 github.com/outside/module というモジュールをプロジェクトルートの ./outside/module/ に置いたとして説明します
  2. そのモジュールに go.mod がなければ go mod init で作成しておく
  3. 利用側の go.mod に、以下のような replace ディレクティブを追記する
    • replace github.com/outside/module => ./outside/module
  4. 手元のモジュールを修正し、非公開フィールドを返す外部メソッドを生やすなり自由に改造する*3

この方法なら、非公開メソッドの呼び出しも可能になります。

たとえば、 func (s *OuterStruct) privateMethod という非公開メソッドがあったとき、それを呼び出すだけの公開メソッド func (s *OuterStruct) CallPrivateMethod を定義し、利用側からはそれを呼ぶようにすれば、最小限の変更で非公開メソッドを呼ぶことができます。

Pros

  • 非公開メソッドも呼べるなど、自由度が高い
    • panic やプリントデバッグなどの命令を仕込んだりもできる

Cons

  • リポジトリに外部のソースコードをそのまま持ってきて改変するため、バージョンアップが大変になったり、元のソースからの diff が管理しづらい
    • ライセンスにも注意が必要

参考

*1:この URL ではそのうちアクセスできなくなると思います

*2:この方法は @ikawaha さんに教えていただきました。ありがとうございました!

*3:ライセンスに注意!