私が歌川です

@utgwkk が書いている

reflectパッケージでstructのunexportedなフィールドにアクセスするイディオムの正当性を (一部) 確かめる

生きてるといろいろなことがあり、リフレクションでstructのunexportedなフィールドに値を書き込みたくて調べていたら、以下のStackoverflowの回答が見つかった。

stackoverflow.com

以下のようなイディオムで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である、と書いてある。

  1. Conversion of a *T1 to Pointer to *T2.
  2. Conversion of a Pointer to a uintptr (but not back to Pointer).
  3. Conversion of a Pointer to a uintptr and back, with arithmetic.
  4. Conversion of a Pointer to a uintptr when calling syscall.Syscall.
  5. Conversion of the result of reflect.Value.Pointer or reflect.Value.UnsafeAddr from uintptr to Pointer.
  6. 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() メソッドを呼んで得られた uintptrunsafe.Pointer に変換している。なので、確かに 5. の条件を満たす。

以降のreflectパッケージを使った処理の動作原理については、先述したブログ記事で解説されているので、そちらを参照してほしい。しかしunsafeっていう言葉が出てくると身が引き締まりますね。

*1:完全なコードは https://go.dev/play/p/Fn48VJfNO9a にある