Go1.15 からのreflectパッケージの挙動の違いについて

追記

Go1.15 からのreflectパッケージの挙動の違いについて | Zenn にも掲載してみました。

概要

Go1.15からreflectパッケージの一部挙動が変わった。これにより、Go1.14以前で動作していたコードがPanicしうる。

非公開構造体を埋め込み、その構造体のメソッドをCallした場合にPanicする。

目次

リリースノートの記述

golang.org

Go 1.15のリリースノートによると、reflectパッケージについて下記記述がありました。

Package reflect now disallows accessing methods of all non-exported fields, whereas previously it allowed accessing those of non-exported, embedded fields. Code that relies on the previous behavior should be updated to instead access the corresponding promoted method of the enclosing variable.

サンプルコード

サンプルコードは下記です。二つの構造体を定義しています。

  • 非公開のunexportedStruct Printするだけのメソッドを持つ。
  • 公開のR 上記unexportedStructが埋め込まれている。

Rを用いて、unexportedStruct内のメソッドにアクセスしようとするコードです。エラー処理は省いています。

package main

import (
    "fmt"
    "reflect"
)

type unexportedStruct struct{}

func (s unexportedStruct) Print() {
    fmt.Println("Print!")
}

type R struct {
    unexportedStruct
}

func main() {
    v := reflect.ValueOf(R{})
    fmt.Printf("NumMethod=%d\n", v.Field(0).NumMethod())

    method := v.Field(0).Method(0)
    fmt.Printf("Method=%v\n", method)
    method.Call(nil)
}

Go1.14とGo1.15の違い

下記が挙動の違いです。Go 1.14.7 ではPrint関数が実行されるのに対し、Go1.15ではPanicして落ちます。

Go 1.15

$ go version
go version go1.15 linux/amd64
$ go run unexported.go 
NumMethod=1
Method=0x48f340
panic: reflect: reflect.Value.Call using value obtained using unexported field

goroutine 1 [running]:
reflect.flag.mustBeExportedSlow(0x2b3)
    /usr/local/go/src/reflect/value.go:237 +0x131
reflect.flag.mustBeExported(...)
    /usr/local/go/src/reflect/value.go:228
reflect.Value.Call(0x4dd980, 0x5daf48, 0x2b3, 0x0, 0x0, 0x0, 0x1, 0x10, 0x0)
    /usr/local/go/src/reflect/value.go:335 +0x52
main.main()
    /home/taka/tmp/unexported.go:22 +0x1ef
exit status 2

Go 1.14

$ go version
go version go1.14.7 linux/amd64
$ go run unexported.go 
NumMethod=1
Method=0x488b20
Print!

修正コミット

この辺ですかね。

github.com

所感

Go1.14までの挙動がバグっぽいので適切に修正されたのかなと。

けれどもユーザーとしてはPanicするか否かをどう判別するのがいいんでしょうかね。いずれもNumMethod()の返り値は同じですし、CanCallのようなチェック関数もないですし。