🛠️

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

2020/09/24に公開

自ブログからの転載です。

概要

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

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

リリースノートの記述

https://golang.org/doc/go1.15#reflect

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!

修正コミット

この辺ですかね。

https://github.com/golang/go/commit/0eb694e9c217c051cd8cc18258bf593d0be7fb8d#diff-5a4d1c9b8c6ee0bc677232e253616a23

所感

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

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

Discussion