生きてるといろいろなことがあり、リフレクションでstructのunexportedなフィールドに値を書き込みたくて調べていたら、以下のStackoverflowの回答が見つかった。
以下のようなイディオムでstructのunexportedなフィールドにアクセスできる*1。
type S struct { a int b int } s := S{} rv := reflect.ValueOf(&s).Elem() for i := 0; i < rv.NumField(); i++ { field := rv.Field(i) field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() field.SetInt(1000) } fmt.Printf("%#v\n", s)
このイディオムでunexportedなフィールドの値を読み書きできるようになる原理は goでreflectを使ってunexported fieldの値を見る - podhmo's diary で解説されているけど、ここで unsafe.Pointer
を使ってよい理由がパッと分からなかった。
元のStackoverflowの回答には以下のように書いてあるので、根拠を確かめることにする。
This use of unsafe.Pointer is valid according to the documentation and running
go vet
returns no errors.
unsafe.Pointer
のドキュメントには、以下のパターンで unsafe.Pointer
を使うのはvalidである、と書いてある。
- Conversion of a
*T1
to Pointer to*T2
. - Conversion of a Pointer to a uintptr (but not back to Pointer).
- Conversion of a Pointer to a uintptr and back, with arithmetic.
- Conversion of a Pointer to a uintptr when calling syscall.Syscall.
- Conversion of the result of reflect.Value.Pointer or reflect.Value.UnsafeAddr from uintptr to Pointer.
- Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.
このうちreflectパッケージが関係するのは 5. だけなので、5. の条件を満たすことを確認する。
unsafe.Pointer
を使っている部分だけに着目すると、以下のような式になっていることが分かる。
unsafe.Pointer(field.UnsafeAddr())
この式では、 reflect.Value
型の UnsafeAddr()
メソッドを呼んで得られた uintptr
を unsafe.Pointer
に変換している。なので、確かに 5. の条件を満たす。
以降のreflectパッケージを使った処理の動作原理については、先述したブログ記事で解説されているので、そちらを参照してほしい。しかしunsafeっていう言葉が出てくると身が引き締まりますね。
*1:完全なコードは https://go.dev/play/p/Fn48VJfNO9a にある