フューチャー技術ブログ

たのしいGoリフレクション: 変数アサイン

The Go gopher was designed by Renée French

一般に、リフレクションは黒魔術と呼ばれることもあります。Goでは「リフレクションよりはコード生成」と長く言われてきました。ジェネリクスも一般的にはそのリフレクションとコード生成の間を埋めるもので、やはりリフレクションの使用を減らすためのもの(リフレクションではできない静的型チェックもできますが)です。

ですが、リフレクションでしか実現できないこともあります。そのため、誰かが人柱にならなければならないこともあります。リフレクションについては次のサイトがよくまとまっています。

ですが、やってみると予想外のパニックがいっぱい出てきます。まあパニックといっても、スタックトレースとかがきちんと出てくれるので、昔のC++と比べるとだいぶ優しい世界ですが。

構造体のタグを見て、データを取り出したり、データを構造体に設定したりする、encoding/jsonのようなライブラリを自分で作りたい時のヘルパーライブラリとして作ったのが以下のものです。Goで2WaySQLを実現するgithub.com/future-architect/go-twowaysqlで、パラメータを構造体から取り出したり、実行結果を構造体にマッピングするところで使われています。この関数は独立した関数としてパッケージ外からも使えるようにしています。

これを作ったのはだいぶ前ですが(最近future-architect orgに移動してきた)、最近ちょっとバグ修正したりして、またハマったりしたので、また将来メンテするときのためのメモとして残しておきます。

ポインタがnilに初期化されていると型情報の取り方に注意が必要

reflect.Valueがすべての基本となる型です。

  • Elem()メソッド→ポインタのデリファレンス。*int*の記号
  • Type()メソッド→型情報を取得

*intの変数から、intという型情報を取り出すのは、Type()Elem()でもElem()Type()でもどちらでも到達できるのですが、変数がnilの場合だけ先にElem()を呼ぶとパニックになります。

package main

import (
"fmt"
"reflect"
)

func main() {
// nilじゃない
var i *int = &[]int{1}[0]
// nil
var j *int

vi := reflect.ValueOf(i)
fmt.Println(vi.Type().Elem())
fmt.Println(vi.Elem().Type())

vj := reflect.ValueOf(j)
fmt.Println(vj.Type().Elem())
fmt.Println(vj.Elem().Type()) // panic
}

代入時はポインタでないといけない

変数に代入するときはポインタを渡す必要があります。ポインタのreflect.ValueElem()と、即値のreflect.Valueは、どちらもintですが、代入が可能かどうかが違います。playgroundとかで試しながらやっていて「いけるはずなのに」と思ってだいぶハマったポイントです。

package main

import (
"fmt"
"reflect"
)

func main() {
var i int

vip := reflect.ValueOf(&i) // ポインタにしないといけない
fmt.Println(vip.Elem().Type(), vip.Elem().CanSet(), vi.Elem().CanAddr())
// int true true
vip.Elem().SetInt(10)
fmt.Printf("i = %d\n", i)

vi := reflect.ValueOf(i)
fmt.Println(vi.Type(), vi.CanSet(), vi.CanAddr())
// int false false
vi.SetInt(20) // panic
fmt.Printf("i = %d\n", i)
}

reflect.ValueOfに渡す時に、値渡しになってしまうと上書き不可になってしまうようです。C言語脳とか、Goのメソッドのレシーバーがポインタか値かのイメージでいると、スタックメモリに乗って書き換えは可能だけど、結果は呼び出し元には帰ってこない、みたいな感じを期待しちゃうのですがpanic()になります。CanAddr()で、ポインタ取得かのうかどうかもfalseに。

構造体も最初がポインタでないとエラーになる

構造体のメンバーは値型であればFieldByName()Field()で取ってきたreflect.Valueはポインタではなくてその該当する型になります。前節の変数のポインタ渡しと違って、Elem()は呼ばずに書き換えできるのですが、これも最初の構造体をポインタで渡さないとだめです。

package main

import (
"fmt"
"reflect"
)

func main() {
s := struct{ Name string }{}

vps := reflect.ValueOf(&s).Elem()
vf := vps.FieldByName("Name")
fmt.Println(vf.Type(), vf.CanSet(), vf.CanAddr())
// string true true
vf.SetString("hello")

vs := reflect.ValueOf(s)
vf2 := vs.FieldByName("Name")
fmt.Println(vf2.Type(), vf2.CanSet(), vf2.CanAddr())
// string false false
vf2.SetString("hello") // panic
}

代入先がポインタ変数でnilの場合は先にnewする

これはGoで普通にコードを書いていたら当たり前のことですが、リフレクションとして再現する時にちょっと手間取ったところなので。通常はポインタ変数があったときに、ポイント先が無効(nil)なら値の設定はしないのですが、encoding/jsonなどはポイント先のメモリを確保した上で値を設定してくれるので、その挙動の再現をしようと思います。

参照先のメモリが有効であれば、ポインタを値渡しして設定も可能です。

リフレクションでnewするにはreflect.New()をします。reflect.Typeが必要なので、即値の0からreflect.TypeOfで型情報にしてやっています。

なお、「代入時はポインタでないといけない」ルールは、元々の変数がポインタ型であっても、今回のようにnewしたインスタンスを割り当てる場合はやはり該当しますので、

package main

import (
"fmt"
"reflect"
)

func main() {
var i *int = &[]int{0}[0] // 初期化済みポインタ変数

// 初期化済みならポインタ変数の値渡しも可能
vip := reflect.ValueOf(i)
fmt.Println(vip.Type().Elem(), vip.Elem().CanSet(), vip.Elem().CanAddr())
// int false false: ポインタが指す先は代入可能
vip.Elem().SetInt(20) // panic
fmt.Printf("i = %d\n", *i)

var j *int // ポインタ変数

// ポインタ変数のポインタ渡し
vjpp := reflect.ValueOf(&j)
fmt.Println(vjpp.Type().Elem(), vjpp.Elem().CanSet(), vjpp.Elem().CanAddr())
// *int true true: ポインタ変数自体は代入可能
fmt.Println(vjpp.Type().Elem().Elem(), vjpp.Elem().Elem().CanSet(), vjpp.Elem().Elem().CanAddr())
// int false false: ポインタが指す先は代入不可能
vjpp.Elem().Set(reflect.New(reflect.TypeOf(0))) // 初期化
fmt.Println(vjpp.Type().Elem().Elem(), vjpp.Elem().Elem().CanSet(), vjpp.Elem().Elem().CanAddr())
// int true true: 初期化したら代入可能になった
vjpp.Elem().Elem().SetInt(20) // panic
fmt.Printf("j = %d\n", *j)

var k *int // ポインタ変数

// ポインタ変数の値渡し
vkp := reflect.ValueOf(k)
fmt.Println(vkp.Type(), vkp.CanSet(), vkp.CanAddr())
// *int false false: ポインタ変数自体が代入不可能に
vkp.Set(reflect.New(reflect.TypeOf(0))) // 初期化でpanic
// vjp.Elem().SetInt(20)
// fmt.Printf("j = %d\n", *j)
}

ちょっとした型違いであっても代入は可能

初めてリフレクションの値の代入をしたときは、変換元と変換先の型を大量に並べた巨大なswitch文を書いてやっていたのですが、型違いであってもreflect.ValueConvert()メソッドで変換できます。panicを防ぐためにあらかじめCanConvert()メソッドで確認しておくと安全です。

package main

import (
"fmt"
"reflect"
)

func main() {
var f float64

vfp := reflect.ValueOf(&f)
vft := vfp.Type().Elem()

// intはfloat64に変換可能
fmt.Println(reflect.ValueOf(10).CanConvert(vft))
// true

// 変換したら設定可能
vfp.Elem().Set(reflect.ValueOf(10).Convert(vft))
fmt.Println(f)

// 変換しないとpanic
vfp.Elem().Set(reflect.ValueOf(20)) // panic
// fmt.Println(f)
}

でもやっちゃいけないこともある

数字から文字列への変換はたぶんruneとして扱われて成功しちゃうのですが、一般的にはやりたいことではないと思うので、CanConvert()そのままではなく、除外したい条件も設定する必要があります。

package main

import (
"fmt"
"reflect"
)

func main() {
var s string

vsp := reflect.ValueOf(&s)
vst := vsp.Type().Elem()

// intはstringに変換可能?
fmt.Println(reflect.ValueOf(1234).CanConvert(vst))
// true
vsp.Elem().Set(reflect.ValueOf(1234).Convert(vst))
fmt.Println(s)
// Ӓ という文字になってしまう

ik := reflect.TypeOf(0).Kind()
// この変換を封じるための条件式
fmt.Println(reflect.ValueOf(1234).CanConvert(vst) && !(vst.Kind() == reflect.String && ((ik == reflect.Int) || (ik == reflect.Uint))))
// false
}

まとめ

同じコードの書き方でも、状況によってうまくいったりいかなかったりということがあって、思ったよりもコードのデバッグに時間がかかってしまったので、整理した結果をまとめました。リファレンスを眺めただけではわからなかった数々の落とし穴です。

Type()を見た時に同じintとなるreflect.Valueでも、代入可能なもの、不可能なものがある、というのに気づくまでかなり時間がかかって遠回りをしてきましたが、今では自信を持ってリフレクションが使えるようになりました。

  • nilのポインタ変数場合はElem()からType()はpanicになるので逆の呼び出しが必要
  • 単体の変数はポインタ渡し。Elem()を呼んでからSet()系メソッドが必要
  • 構造体のメンバーは値型の場合はElem()不要
  • ポインタ変数への代入はインスタンスを作ってあげる必要がある
  • ちょっとした自動型変換はできるが、数値から文字列への変換は要注意

リフレクションをやる必要がある人は、参考にしてもらえればと思います。