24
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

QualiArtsAdvent Calendar 2020

Day 17

Go リフレクション入門

Last updated at Posted at 2020-12-16

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先生に翻訳してもらうと、以下のようになります。

  1. リフレクションは、インターフェイス値からリフレクションオブジェクトに移動します。
  2. リフレクションは、リフレクションオブジェクトからインターフェイス値に移動します。
  3. リフレクションオブジェクトを変更するには、値を設定可能にする必要があります。

順を追って見ていきましょう。
なお、本記事のサンプルでは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は型の種類を表す値で、以下のように定義されています。

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毎に処理を分岐して個別の処理を記述していく形になります。

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()メソッドにより判定することができます。
リフレクションオブジェクト経由で元の値を書き換える手順は以下のようになります。

  1. 更新したい値のポインタからリフレクションオブジェクト(reflect.Value)を生成
  2. 1で取得したリフレクションオブジェクトのElem()メソッドでポインタが指す値のリフレクションオブジェクトを取得
  3. 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つとして頭の片隅においておくと良いかもしれません。
以上、最後までお読みいただきありがとうございました。

24
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?