😽

Goのリフレクションの使い方

2024/10/26に公開

まとめ

リフレクションはあらゆる変数をinterface{}として扱った上で、変数の実際の値に基づいて柔軟な(動的型付け言語みたいな)操作を実現する機能。

そもそもリフレクションとは

※以下、Goのリフレクションの話
https://go.dev/blog/laws-of-reflection

Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion.

Goの型の振り返り

type MyInt int

var i int    // iにMyInt型の値は型変換しないと代入できない
var j MyInt  // jにint型の値は型変換しないと代入できない

このとき、iとjのunderlying typeはどちらもintなのに、型変換が必要になる。これは当たり前。

type Greeter interface { Hello(name string) }
type Person struct { name string, age int }
(p *Person) Hello(name string) { fmt.Printf("Hello, %s!\n", name) }

上記ではHelloメソッドを実装しているので、PersonはGreeterインターフェースを満たす。
このように、
指定されたメソッドさえ実装していればどんな値であれinterface型として扱える。

interface{}(またはaliasのany)は、何のメソッドも指定していないからどんな型でもこのインターフェースを満たす。
このことによって、interface{}型の実際の値は実行中にどんな値の型にも変更できる。
そしてそれらの型はinterface{}型として扱える。

Goのインターフェースの振り返り

※詳細は Go Data Structures: Interfacesを参照。

インターフェース型の変数は、変数の実際の値と、その値の型の識別子を持つ。

type Greeter interface { Hello(name string) }
(p *Person) Hello(name string) { fmt.Printf("Hello, %s!\n", name) }

type Runner interface { Run() }
(p *Persion) Run() { fmt.Println("running...") }

var g Greeter
p := Person{name: "John", age: 20}
p.Hello("James") // Hello, James!
g = p

上記のときgはGreeter型だが、以下のような2つの情報を内部的に保持する

  • 実際の値: { name: "John", age: 20 }
  • 値の型の識別子: Person

これによって、以下のような型変換が可能になる。

var r Runner
r = g.(Runner)

上記では、Greeter型をRunner型に変換している。
これは、以下の条件が満たされるから可能になる

  • g(Greeter型)が実際の値の型(Person)を知っている
  • PersonはRunnerインターフェースを満たす

このとき、rはRunner型だが、gと同じ情報を内部的に保持する

  • 実際の値: { name: "John", age: 20 }
  • 値の型の識別子: Person

同様に、以下も可能。

var empty interface{}
empty = r

そしてemptyはinterface{}型だが、g, rと同じ情報を内部的に保持する。

改めて、Goのリフレクションとは

interface{}という柔軟な型で定義した変数について、実際に格納された値とその型に基づいた振る舞いをさせる機能。

リフレクションは以下の3つのルールを満たす。

  1. インターフェースの値からリフレクションオブジェクトに変換可能
  2. リフレクションオブジェクトからインターフェースの値に変換可能
  3. リフレクションオブジェクトを編集するためには、値をセットできる必要がある

1. インターフェースの値からリフレクションオブジェクトに変換可能

インターフェース型の変数に格納された値とその型を検査する仕組み。
格納された値はreflectパッケージのValue、その型はTypeで確認できる。

func main() {
	error := fmt.Errorf("error")
	fmt.Println("type:", reflect.TypeOf(error)) // type: *errors.errorString
}

このとき、reflect.TypeOfのシグネチャは以下の通り。

func TypeOf(i interface{}) Type 

したがって、上記のコードのerrorはinterface{}として扱われている

また、reflect.ValueOf()をつかって、interface{}が内部的に格納した値を確認できるし、Kindを使ってinterface{}に格納された値をreflectではどう表現するのかを確認できる

func main() {
	error := fmt.Errorf("error")
	value := reflect.ValueOf(error)
	fmt.Println("type:", value.Type()) // type: *errors.errorString
	fmt.Println("kind is pointer:", value.Kind() == reflect.Ptr) // kind is pointer: true
	fmt.Println("type:", value.String()) // type: <*errors.errorString Value>
}

https://go.dev/play/p/S2iCEMJEAI7

なお、Kindは格納した値のunderlying typeを取得する。
したがって、下記のような場合MyIntではなくintが返される。


func main() {
	type MyInt int
	var x MyInt = 7
	v := reflect.ValueOf(x)
	fmt.Println(v.Kind()) // int(MyIntではない!)
}

2. リフレクションオブジェクトからインターフェースの値に変換可能

以下のようにValueOf(i any) Valueの戻り値を与えるとinterface{}が返されるメソッドがある。

func (v Value) interface() interface{}

以下のようにxから取り出されたValue(リフレクションオブジェクト)をinterface{}に変換し、それをキャストしてfloat64として扱える。

func main() {
	type MyInt int
	var x MyInt = 7
	v := reflect.ValueOf(x)
	y := v.Interface().(float64)
	fmt.Println(y)
}

※実際にはinterface{}からfloat64へのキャストはPrintlnの内部で勝手に行われるから、上記のように明示的に行う必要はない

3. リフレクションオブジェクトを編集するには、その値がSettableである必要がある

Settableとは、ポインタ渡しによる変数の書き換え可能性(Addressability)に近い概念。

Addressability

たとえば以下のように、person変数の値を関数に渡すか、person変数のポインタを渡すかによって、person変数のプロパティが書き換えられるかは変わる。

以下は値渡しとポインタ渡しの違いの例


type Person struct {
	Age int
}

func stayYoung(p Person) {
	p.Age += 1
}

func getOld(p *Person) {
	p.Age += 1
}

func main() {
	person := Person{
		Age: 19,
	}
	stayYoung(person)
	fmt.Println(person.Age) // 19
	getOld(&person)
	fmt.Println(person.Age) // 20
}

Settability

Settabilityはリフレクションオブジェクトが、自身を作った変数自体を書き換える能力。
リフレクションオブジェクト自体が元の値を保持しているかどうかに依存する

func main() {
	var original float64 = 3.4
	copied := reflect.ValueOf(original) // 値渡しではSettabilityは得られない
	fmt.Println(copied.CanSet()) // false

	pointer := reflect.ValueOf(&original) // ポインタ自体はSettableではない
	fmt.Println(pointer.CanSet()) // false

	reflectionValue := pointer.Elem()     // ポインタからoriginal valueを取得すると、Settable
	fmt.Println(reflectionValue.CanSet()) // true
}

上記において、reflectionValueはSettableであるため書き換えが可能。
```go
	reflectionValue.SetFloat(7.1)
	fmt.Println(reflectionValue) // 7.1
}

Discussion