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 |
代入時はポインタでないといけない
変数に代入するときはポインタを渡す必要があります。ポインタのreflect.Value
のElem()
と、即値のreflect.Value
は、どちらもint
ですが、代入が可能かどうかが違います。playgroundとかで試しながらやっていて「いけるはずなのに」と思ってだいぶハマったポイントです。
package main |
reflect.ValueOf
に渡す時に、値渡しになってしまうと上書き不可になってしまうようです。C言語脳とか、Goのメソッドのレシーバーがポインタか値かのイメージでいると、スタックメモリに乗って書き換えは可能だけど、結果は呼び出し元には帰ってこない、みたいな感じを期待しちゃうのですがpanic()
になります。CanAddr()
で、ポインタ取得かのうかどうかもfalse
に。
構造体も最初がポインタでないとエラーになる
構造体のメンバーは値型であればFieldByName()
やField()
で取ってきたreflect.Value
はポインタではなくてその該当する型になります。前節の変数のポインタ渡しと違って、Elem()
は呼ばずに書き換えできるのですが、これも最初の構造体をポインタで渡さないとだめです。
package main |
代入先がポインタ変数でnil
の場合は先にnewする
これはGoで普通にコードを書いていたら当たり前のことですが、リフレクションとして再現する時にちょっと手間取ったところなので。通常はポインタ変数があったときに、ポイント先が無効(nil)なら値の設定はしないのですが、encoding/jsonなどはポイント先のメモリを確保した上で値を設定してくれるので、その挙動の再現をしようと思います。
参照先のメモリが有効であれば、ポインタを値渡しして設定も可能です。
リフレクションでnew
するにはreflect.New()
をします。reflect.Type
が必要なので、即値の0からreflect.TypeOf
で型情報にしてやっています。
なお、「代入時はポインタでないといけない」ルールは、元々の変数がポインタ型であっても、今回のようにnew
したインスタンスを割り当てる場合はやはり該当しますので、
package main |
ちょっとした型違いであっても代入は可能
初めてリフレクションの値の代入をしたときは、変換元と変換先の型を大量に並べた巨大なswitch文を書いてやっていたのですが、型違いであってもreflect.Value
のConvert()
メソッドで変換できます。panicを防ぐためにあらかじめCanConvert()
メソッドで確認しておくと安全です。
package main |
でもやっちゃいけないこともある
数字から文字列への変換はたぶんruneとして扱われて成功しちゃうのですが、一般的にはやりたいことではないと思うので、CanConvert()
そのままではなく、除外したい条件も設定する必要があります。
package main |
まとめ
同じコードの書き方でも、状況によってうまくいったりいかなかったりということがあって、思ったよりもコードのデバッグに時間がかかってしまったので、整理した結果をまとめました。リファレンスを眺めただけではわからなかった数々の落とし穴です。
Type()
を見た時に同じint
となるreflect.Value
でも、代入可能なもの、不可能なものがある、というのに気づくまでかなり時間がかかって遠回りをしてきましたが、今では自信を持ってリフレクションが使えるようになりました。
nil
のポインタ変数場合はElem()
からType()
はpanicになるので逆の呼び出しが必要- 単体の変数はポインタ渡し。
Elem()
を呼んでからSet()
系メソッドが必要 - 構造体のメンバーは値型の場合は
Elem()
不要 - ポインタ変数への代入はインスタンスを作ってあげる必要がある
- ちょっとした自動型変換はできるが、数値から文字列への変換は要注意
リフレクションをやる必要がある人は、参考にしてもらえればと思います。