QualiArts Advent Calendar 2020、17日目担当のs9iです。
今回はGoにおけるリフレクションの基本的な使用方法について書いていきます。
リフレクションとは
プログラム実行時に、動的にプログラムの構造を読み取ったり書き換えたりする手法です。
可読性が低い、パフォーマンスが悪い等の理由により使う機会は多くはありませんが、通常の実装では手が出せないようなことができる場合もあります。
Goにおけるリフレクション
リフレクションの機能は標準パッケージのreflectで提供されています。
Goでのリフレクションは、以下の法則に則ります。
1.Reflection goes from interface value to reflection object.
2.Reflection goes from reflection object to interface value.
3.To modify a reflection object, the value must be settable.
The Laws of Reflection - The Go Blog
Google先生に翻訳してもらうと、以下のようになります。
- リフレクションは、インターフェイス値からリフレクションオブジェクトに移動します。
- リフレクションは、リフレクションオブジェクトからインターフェイス値に移動します。
- リフレクションオブジェクトを変更するには、値を設定可能にする必要があります。
順を追って見ていきましょう。
なお、本記事のサンプルではGo 1.15.5を使用しています。
リフレクションオブジェクトの取得
まず1つ目の法則です。
Reflection goes from interface value to reflection object. (リフレクションは、インターフェイス値からリフレクションオブジェクトに移動します。)
GoではJAVAやC#のように、型定義から直接リフレクションオブジェクトを生成することはできません。interface{}型の値からリフレクションオブジェクトを生成する必要があります。
型情報の取得
reflect.TypeOf関数により、型の情報を表すreflect.Typeを取得できます。
func TypeOf(i interface{}) Type
func main() {
stringValue := "test"
intValue := 12345
fmt.Println("=== int ===")
print(intValue)
fmt.Println("=== string ===")
print(stringValue)
fmt.Println("=== *string ===")
print(&stringValue)
}
func print(v interface{}) {
tv := reflect.TypeOf(v)
fmt.Println("Kind:", tv.Kind(), "Name:", tv.Name())
}
=== int ===
Kind: int Name: int
=== string ===
Kind: string Name: string
=== *string ===
Kind: ptr Name:
Kindは型の種類を表す値で、以下のように定義されています。
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
リフレクションを使った実装では、基本的にKind毎に処理を分岐して個別の処理を記述していく形になります。
switch t := reflect.TypeOf(v); t.Kind() {
case reflect.String:
fmt.Println(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Println(v.Int())
default:
...
}
値情報の取得
reflect.ValueOf関数により、値の情報を表すreflect.Valueを取得できます。
先述のreflect.Typeと同名のメソッドもありますが、挙動は異なるので注意が必要です。
func ValueOf(i interface{}) Value
Interface()メソッドでinterface{}としての値を取得することができます。
func main() {
stringValue := "test"
intValue := 12345
fmt.Println("=== int ===")
printValue(intValue)
fmt.Println("=== string ===")
printValue(stringValue)
fmt.Println("=== *string ===")
printValue(&stringValue)
}
func printValue(v interface{}) {
rv := reflect.ValueOf(v)
fmt.Println("Kind:", rv.Kind(), "Type:", rv.Type(), "Interface:", rv.Interface())
}
=== int ===
Kind: int Type: int Interface: 12345
=== string ===
Kind: string Type: string Interface: test
=== *string ===
Kind: ptr Type: *string Interface: 0xc000010200
構造体の情報取得
フィールド情報の取得
構造体の型情報や値情報を取得し、Field()やFieldByName()を使用することでフィールドの情報を取得することができます。
type User struct {
NickName string
Age int32
}
func main() {
user := User{
NickName: "user1",
Age: 30,
}
fmt.Println("=== Type ===")
tv := reflect.TypeOf(user)
fmt.Println("Kind:", tv.Kind())
fmt.Println("Name:", tv.Name())
fmt.Println("NumField:", tv.NumField())
fmt.Println("Field:", tv.Field(0))
f, _ := tv.FieldByName("Age") // フィールドが存在しない場合は第二戻り値がfalseを返す
fmt.Println("FieldByName:", f)
fmt.Println()
fmt.Println("=== Value ===")
rv := reflect.ValueOf(user)
fmt.Println("Kind:", rv.Kind())
fmt.Println("Type:", rv.Type())
fmt.Println("Interface:", rv.Interface())
fmt.Println("NumField:", rv.NumField())
fmt.Println("Field:", rv.Field(0))
fmt.Println("FieldByName:", rv.FieldByName("Age"))
}
=== Type ===
Kind: struct
Name: User
NumField: 2
Field: {NickName string 0 [0] false}
FieldByName: {Age int32 16 [1] false}
=== Value ===
Kind: struct
Type: main.User
Interface: {user1 30}
NumField: 2
Field: user1
FieldByName: 30
全フィールドを参照したい場合は、NumField()でフィールド数を取得して、ループでField()を使用して各フィールドにアクセスします。
type User struct {
NickName string
Age int32
}
func main() {
user := User{
NickName: "user1",
Age: 30,
}
fmt.Println("=== Type ===")
tv := reflect.TypeOf(user)
for i := 0; i < tv.NumField(); i++ {
t := tv.Field(i)
fmt.Println("Name:", t.Name, "Type:", t.Type)
}
fmt.Println()
fmt.Println("=== Value ===")
rv := reflect.ValueOf(user)
for i := 0; i < rv.NumField(); i++ {
v := rv.Field(i)
fmt.Println("Kind:", v.Kind(), "Type:", v.Type())
}
}
=== Type ===
Name: NickName Type: string
Name: Age Type: int32
=== Value ===
Kind: string Type: string
Kind: int32 Type: int32
メソッド情報の取得
Method()やMethodByName()を使用してメソッドの情報も取得できます。
フィールドの場合と同様に、NumMethod()でメソッド数を取得してループで各メソッドを参照することも可能です。
type User struct {
NickName string
Age int32
}
func (u User) GetNickName() string {
return u.NickName
}
func (u User) GetAge() int32 {
return u.Age
}
func main() {
user := User{
NickName: "user1",
Age: 30,
}
fmt.Println("=== Type ===")
tv := reflect.TypeOf(user)
tm, _ := tv.MethodByName("GetAge") // メソッドが存在しない場合は第二戻り値がfalseを返す
fmt.Println("Name:", tm.Name, "Type:", tm.Type)
for i := 0; i < tv.NumMethod(); i++ {
t := tv.Method(i)
fmt.Println(i+1, "Name:", t.Name, "Type:", t.Type)
}
fmt.Println()
fmt.Println("=== Value ===")
rv := reflect.ValueOf(user)
rm := rv.MethodByName("GetAge")
fmt.Println("Kind:", rm.Kind(), "Type:", rm.Type())
for i := 0; i < rv.NumMethod(); i++ {
v := rv.Method(i)
fmt.Println(i+1, "Kind:", v.Kind(), "Type:", v.Type())
}
}
=== Type ===
Name: GetAge Type: func(main.User) int32
1 Name: GetAge Type: func(main.User) int32
2 Name: GetNickName Type: func(main.User) string
=== Value ===
Kind: func Type: func() int32
1 Kind: func Type: func() int32
2 Kind: func Type: func() string
フィールドのスコープ
reflectにおいては非公開のフィールドの情報も取得することができますが、一部のメソッドを実行するとpanicが発生します。また、後述の値の書き換えを行うこともできません。公開フィールドかどうかはreflect.TypeのPkgPathが空文字かどうかで判定することが可能です。NumField()の数には非公開のフィールドも含まれているので、この値を用いてループを行う場合は注意しましょう。
type User struct {
NickName string
Age int32
password string
}
func main() {
user := User{
NickName: "user1",
Age: 30,
password: "pass",
}
tv := reflect.TypeOf(user)
rv := reflect.ValueOf(user)
f := rv.FieldByName("password")
fmt.Println("String:", f.String())
// panicが発生する
// fmt.Println("Interface:", f.Interface())
for i := 0; i < rv.NumField(); i++ {
t := tv.Field(i)
v := rv.Field(i)
fmt.Println("=== ", t.Name, "===")
fmt.Println("Kind:", v.Kind())
fmt.Println("Type:", v.Type())
// 公開フィールドの場合は空文字
fmt.Println("PkgPath:", t.PkgPath)
if t.PkgPath == "" {
fmt.Println("Interface:", v.Interface())
}
}
}
String: pass
=== NickName ===
Kind: string
Type: string
PkgPath:
Interface: user1
=== Age ===
Kind: int32
Type: int32
PkgPath:
Interface: 30
=== password ===
Kind: string
Type: string
PkgPath: main
メソッドのスコープ
フィールドとは異なり、非公開のメソッドは参照することができず、NumMethod()の数にも含まれません。
type User struct {
NickName string
Age int32
Password string
Address string
}
func (u User) GetNickName() string {
return u.NickName
}
func (u *User) GetAge() int32 {
return u.Age
}
func (u User) getPassword() string {
return u.Password
}
func (u *User) getAddress() string {
return u.Address
}
func main() {
user := User{
NickName: "user1",
Age: 30,
Password: "pass",
Address: "address",
}
tv := reflect.TypeOf(user)
// 非公開のメソッドは参照できない
_, ok := tv.MethodByName("getPassword")
fmt.Println("IsReferable:", ok)
fmt.Println("=== Type ===")
for i := 0; i < tv.NumMethod(); i++ {
t := tv.Method(i)
fmt.Println("Name:", t.Name, "Type:", t.Type)
}
fmt.Println()
fmt.Println("=== Value ===")
rv := reflect.ValueOf(user)
for i := 0; i < rv.NumMethod(); i++ {
v := rv.Method(i)
fmt.Println("Kind:", v.Kind(), "Type:", v.Type())
}
}
実体とポインタのメソッドは区別されるため、GetAge()は出力されません。
IsReferable: false
=== Type ===
Name: GetNickName Type: func(main.User) string
=== Value ===
Kind: func Type: func() string
メソッドの実行
取得したメソッドのreflect.ValueはCall()により実行することができます。
引数、戻り値はreflect.Valueのスライスとなっています。
type User struct {
NickName string
Age int32
}
func (u User) GetAge() int32 {
return u.Age
}
func (u User) CalcAge(year int32) (int32, error) {
return u.Age + year, errors.New("error_message")
}
func main() {
user := User{
NickName: "user1",
Age: 30,
}
rv := reflect.ValueOf(user)
f1 := rv.MethodByName("GetAge")
fmt.Println(f1.Call(nil)[0].Int())
f2 := rv.MethodByName("CalcAge")
args := []reflect.Value{reflect.ValueOf(int32(1))}
rets := f2.Call(args)
fmt.Println(rets[0].Int(), rets[1].Interface())
}
30
31 error_message
タグ情報の取得
jsonエンコード等で馴染みがあるように、構造体のフィールドにはタグを設定することができます。
タグの情報についてもreflect.TypeのTagフィールド(reflect.StructTag型)で参照することが可能です。
Get()で指定タグの値を取得することができます。Lookup()でも指定のタグを取得することができ、こちらは第二戻り値に該当のタグが存在するかどうかの真偽値を返します。
type User struct {
Name string `json:"name"`
Age int32
}
func main() {
user := User{
NickName: "user1",
Age: 30,
}
tv := reflect.TypeOf(user)
for i := 0; i < tv.NumField(); i++ {
f := tv.Field(i)
fmt.Println("===", f.Name, "===")
fmt.Println("Get:", f.Tag.Get("json"))
t, ok := f.Tag.Lookup("json")
fmt.Println("Lookup:", t, ok)
}
}
=== NickName ===
Get: nickName
Lookup: nickName true
=== Age ===
Get:
Lookup: false
スライスの情報取得
スライスの場合はLen()やCap(), Index()といったメソッドが使用できます。
また、Slice()ではスライス式と同等の処理を行います。
func main() {
s := []string{"user1", "user2", "user3"}
rs := reflect.ValueOf(s)
fmt.Println("Kind:", rs.Kind())
fmt.Println("Len:", rs.Len())
fmt.Println("Cap:", rs.Cap())
fmt.Println("Index:", rs.Index(0))
fmt.Println("Type:", rs.Type())
fmt.Println("Interface:", rs.Interface())
fmt.Println("Slice:", rs.Slice(1, 3))
}
Kind: slice
Len: 3
Cap: 3
Index: user1
Type: []string
Interface: [user1 user2 user3]
Slice: [user2 user3]
マップの情報取得
マップの場合はLen()やMapKeys(), MapIndex()といったメソッドが使用できます。
また、MapRange()でイテレータを取得し、各要素を取得することもできます。
func main() {
m := map[int]string{1: "user1", 2: "user2", 3: "user3"}
rm := reflect.ValueOf(m)
fmt.Println("Kind:", rm.Kind())
fmt.Println("Len:", rm.Len())
fmt.Println("Type:", rm.Type())
fmt.Println("MapKeys:", rm.MapKeys())
fmt.Println("MapIndex:", rm.MapIndex(reflect.ValueOf(3)))
iter := rm.MapRange()
for iter.Next() {
fmt.Println("Key:", iter.Key(), "Value:", iter.Value())
}
}
Kind: map
Len: 3
Type: map[int]string
MapKeys: [<int Value> <int Value> <int Value>]
MapIndex: user3
Key: 1 Value: user1
Key: 2 Value: user2
Key: 3 Value: user3
インターフェースの情報取得
ポインタ型を指定することで値がnilの場合でも型情報を取得することができます。更にElem()を使用することでポインタが指す実際の値の型情報を取得できます。
以下の処理では、この方法を利用してインターフェースと構造体の情報を取得しています。Implementsメソッドを使用すると、該当のインターフェースを実装しているか判定することができます。
type User struct {
NickName string
Age int32
}
func (u User) GetNickName() string {
return u.NickName
}
type UserInterface interface {
GetNickName() string
}
func main() {
ri := reflect.TypeOf((*UserInterface)(nil)).Elem()
fmt.Println("===", ri.Name(),"===")
fmt.Println("String:", ri.String())
fmt.Println("Kind:", ri.Kind())
rt := reflect.TypeOf((*User)(nil)).Elem()
fmt.Println("===", rt.Name(),"===")
fmt.Println("String:", rt.String())
fmt.Println("Kind:", rt.Kind())
fmt.Println("User Implements UserInterface:", rt.Implements(ri))
}
=== UserInterface ===
String: main.UserInterface
Kind: interface
=== User ===
String: main.User
Kind: struct
User Implements UserInterface: true
リフレクションオブジェクトによる値の生成
続いて2つ目の法則です。
Reflection goes from reflection object to interface value. (リフレクションは、リフレクションオブジェクトからインターフェイス値に移動します。)
リフレクションオブジェクトから値を生成するには、生成したい型のreflect.Typeを取得し、reflectパッケージで提供される各種生成メソッドを使用します。
基本型の生成
基本型の値を生成するには、reflect.New()を使用します。reflect.New()にreflect.Typeを与えると、該当の型のゼロ値へのポインタを表すリフレクションオブジェクトを取得することができます。interface()で値を取り出し該当の型にキャストするか、各型への変換メソッドを利用するかして実際の値を取得します。
func main() {
tInt := reflect.TypeOf(10)
i1 := *(reflect.New(tInt).Interface().(*int))
i1 = 1
fmt.Println(i1)
i2 := reflect.New(tInt).Elem().Int()
i2 = 2
fmt.Println(i2)
}
1
2
構造体の生成
構造体の生成についても同じようにreflect.New()を使用します。
type User struct {
NickName string
Age int32
}
func main() {
tStruct := reflect.TypeOf(User{})
vStruct := reflect.New(tStruct)
user := vStruct.Interface().(*User)
user.NickName = "user1"
user.Age = 10
fmt.Println(user)
}
&{user1 10}
スライスの生成
スライスの生成にはreflect.MakeSlice()を使用します。
第二引数はスライスの長さ、第三引数はスライスの容量を指定します。
type User struct {
NickName string
Age int32
}
func main() {
tSlice := reflect.TypeOf([]User{})
vSlice := reflect.MakeSlice(tSlice, 0, 2)
users := vSlice.Interface().([]User)
users = append(users, User{
NickName: "user2",
Age: 20,
})
fmt.Println(users)
}
[{user2 20}]
マップの生成
reflect.MakeMap()を使用します。
type User struct {
NickName string
Age int32
}
func main() {
tMap := reflect.TypeOf(map[string]User{})
vMap := reflect.MakeMap(tMap)
userMap := vMap.Interface().(map[string]User)
userMap["hoge"] = User{
NickName: "user3",
Age: 30,
}
fmt.Println(userMap)
}
map[hoge:{user3 30}]
関数の生成
reflect.MakeFunc()を使用します。
第二引数には、reflect.Valueのスライスを引数、戻り値とする関数を指定する必要があります。
type User struct {
NickName string
Age int32
}
func main() {
var intSwap func(int, int) (int, int)
tFunc := reflect.TypeOf(intSwap)
swap := func(in []reflect.Value) []reflect.Value {
return []reflect.Value{in[1], in[0]}
}
vFunc := reflect.MakeFunc(tFunc, swap)
f := vFunc.Interface().(func(int, int) (int, int))
fmt.Println(f(0, 1))
}
1 0
リフレクションオブジェクトの書き換え
最後に3つ目の法則です。
To modify a reflection object, the value must be settable. (リフレクションオブジェクトを変更するには、値を設定可能にする必要があります。)
リフレクションオブジェクトを書き換える場合は、値がセット可能である必要があります。セット不可のリフレクションオブジェクトに対して値を書き換えようとするpanicが発生してしまいます。値のセットが可能かはCanSet()メソッドにより判定することができます。
リフレクションオブジェクト経由で元の値を書き換える手順は以下のようになります。
- 更新したい値のポインタからリフレクションオブジェクト(reflect.Value)を生成
- 1で取得したリフレクションオブジェクトのElem()メソッドでポインタが指す値のリフレクションオブジェクトを取得
- 2で取得したリフレクションオブジェクトのセッターメソッドを利用して値を更新
基本型の書き換え
func main() {
x := 3.4
xv := reflect.ValueOf(x)
fmt.Println("type of xv:", xv.Type())
fmt.Println("kind of xv:", xv.Kind())
fmt.Println("settability of xv:", xv.CanSet())
fmt.Println()
// step1
xp := reflect.ValueOf(&x)
fmt.Println("type of xp:", xp.Type())
fmt.Println("kind of xp:", xp.Kind())
fmt.Println("settability of xp:", xp.CanSet())
fmt.Println()
// step2
xe := xp.Elem()
fmt.Println("type of xe:", xe.Type())
fmt.Println("kind of xe:", xe.Kind())
fmt.Println("settability of xe:", xe.CanSet())
fmt.Println()
// step3
xe.SetFloat(5.0)
fmt.Println(x)
}
type of xv: float64
kind of xv: float64
settability of xv: false
type of xp: *float64
kind of xp: ptr
settability of xp: false
type of xe: float64
kind of xe: float64
settability of xe: true
5
少し複雑に感じるかもしれませんが、そのリフレクションオブジェクトが何を指しているのか(実際の値なのか、ポインタなのか)をしっかり把握しておくと理解しやすいと思います。
構造体の書き換え
構造体のフィールドについても同様の手順で書き換えが可能です。
type User struct {
Name string
Age int32
}
func main() {
u := User{
Name: "user1",
Age: 10,
}
// step1
uv := reflect.ValueOf(u)
fmt.Println("type of uv:", uv.Type())
fmt.Println("kind of uv:", uv.Kind())
fmt.Println("settability of uv:", uv.CanSet())
fmt.Println()
// step2
up := reflect.ValueOf(&u)
fmt.Println("type of up:", up.Type())
fmt.Println("kind of up:", up.Kind())
fmt.Println("settability of up:", up.CanSet())
fmt.Println()
// step3
ue := up.Elem()
fmt.Println("type of ue:", ue.Type())
fmt.Println("kind of ue:", ue.Kind())
fmt.Println("settability of ue:", ue.CanSet())
fmt.Println()
ue.FieldByName("Name").SetString("user2")
fmt.Println(u)
}
type of uv: main.User
kind of uv: struct
settability of uv: false
type of up: *main.User
kind of up: ptr
settability of up: false
type of ue: main.User
kind of ue: struct
settability of ue: true
{user2 10}
まとめ
本記事ではGoのリフレクションの法則や基本的な使用方法について紹介しました。
最後にまとめです。
- reflectパッケージを用いて動的に型の情報を取得、操作することができる
- Goのリフレクションは3つの法則に則る
- 可読性やパフォーマンスを意識しながら用法用量を守って使う
Goでは自動生成を使う機会が多いのでリフレクションを使う機会はあまりないかもしれませんが、うまく使えば有用なケースもあるので、選択肢の1つとして頭の片隅においておくと良いかもしれません。
以上、最後までお読みいただきありがとうございました。