Go言語のreflectパッケージを使った実行時メタプログラミング実践

実行時に型情報を扱うための3原則

  1. インタフェース値 → reflect.Value / reflect.Type への変換が可能
  2. reflect.Value / reflect.Type → インタフェース値 への復元が可能
  3. reflect.Value で値を書き換えるには「アドレス可能」である必要がある

型システムとreflectの関係

Goでは変数は「静的型」と「実行時型(コンクリート型)」を持つ。
静的型はソースコード上で宣言した型であり、実行時型は実際に格納されている値の型である。
reflectは実行時型を扱うため、空インタフェース(interface{})を経由した値のみが対象となる。

reflect.TypeOf / reflect.ValueOf の基礎

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var price float64 = 9.87
	fmt.Println("type :", reflect.TypeOf(price))
	fmt.Println("value:", reflect.ValueOf(price))
}

実行結果

type : float64
value: 9.87

reflect.Value から元の値を取り出す

型が事前に分かっている場合

v := reflect.ValueOf(&price)
ptr := v.Interface().(*float64) // *float64 型アサーション
val := reflect.ValueOf(price).Interface().(float64)

型が分からない場合の探索

type Product struct {
	ID    int
	Title string
	Stock int
}

func inspect(input interface{}) {
	t := reflect.TypeOf(input)
	fmt.Println("concrete type:", t)

	v := reflect.ValueOf(input)
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		val := v.Field(i).Interface()
		fmt.Printf("%s %v = %v\n", f.Name, f.Type, val)
	}

	for i := 0; i < t.NumMethod(); i++ {
		m := t.Method(i)
		fmt.Printf("method %s %v\n", m.Name, m.Type)
	}
}

func main() {
	p := Product{1001, "Keyboard", 42}
	inspect(p)
}

値の書き換え

reflect.Value で値を変更するには、対象がアドレス可能(addressable)でなければならない。
つまり、ポインタを介して Elem() で間接参照した reflect.Value でのみ SetXXX 系メソッドが使える。

func main() {
	var ratio float64 = 0.5
	rv := reflect.ValueOf(&ratio).Elem() // *float64 → float64
	if rv.CanSet() {
		rv.SetFloat(0.75)
	}
	fmt.Println(ratio) // 0.75
}

動的メソッド呼び出し

type Counter struct{ n int }

func (c *Counter) Inc() { c.n++ }
func (c Counter) String() string { return fmt.Sprintf("%d", c.n) }

func callByName(obj interface{}, methodName string) {
	v := reflect.ValueOf(obj)
	m := v.MethodByName(methodName)
	if m.IsValid() {
		m.Call(nil)
	}
}

func main() {
	c := &Counter{}
	callByName(c, "Inc")
	fmt.Println(c) // 1
}

まとめ

  • reflect は interface{} を起点に実行時型情報を取得・操作する
  • TypeOf / ValueOf でメタデータを取得し、Interface() で元の値に戻せる
  • 値の書き換えにはポインタを経由し、Elem() で間接参照した addressable な reflect.Value が必要
  • 構造体のフィールドやメソッドも動的に列挙・呼び出し可能
  • ジェネリクスがない時代の代替手段として、高度な汎用ライブラリで活用されてきた

タグ: Go reflect interface MetaProgramming runtime-type-information

7月20日 17:30 投稿