エムスリーエンジニアリンググループの木村です。 普段はBIRという医療従事者の会員向けアンケートをベースに、製薬会社へのマーケティング支援を提供するチームでソフトウェアエンジニア兼チームSREをやっています。
入社とほぼ同時にGoを書き始め、そろそろ1年が経とうとしています。 主にWebアプリケーションのバックエンドの実装にGoを利用していますが、その他にも小さいCLIツール等もGoで書くことが多くなってきています。
今回は業務でWebアプリケーションを開発していくにあたって疑問が生じた点とその時行ったreflect
パッケージの調査について書いていきます。
gorp
パッケージを利用していて発生した疑問
我々が開発しているアプリケーションではデータベースとのやり取りに gorpパッケージを利用しています。
gorp
パッケージのDbMap
構造体にはInsert
というメソッドが備わっており、以下のように使えます。
// 構造体を定義 type Person struct { Name string `db:"name notnull"` Age int32 `db:"age notnull"` } type Company struct { Name string `db:"name notnull"` Address string `db:"address notnull"` } // Goの構造体とSQLスキーマを対応付け dbmap.AddTableWithName(Person{}, "person") dbmap.AddTableWithName(Company{}, "company") v1 := Person{ Name: "山田太郎", Age: 25, } v2 := Company{ Name: "エムスリー", Address: "東京都港区", } // インサート dbmap.Insert(&v1, &v2)
事前にGoの構造体とデータベースのテーブルスキーマの対応付けを行い、Goの構造体をデータベースのテーブルにインサートしています。
何気ないコードではありますが、よく考えるとこれは少し不思議ではないでしょうか。
Goをある程度書いたことがある人なら想像が付くと思いますが、Insert
メソッドのシグニチャは以下のようになっています。
Insert(list ...interface{}) error
引数の型はinterface{}
となっており、これはGoで任意の型・インタフェースを受け取りたい場合の常套句ですね。
つまり、Insert
メソッドに引数として渡した時点で変数v
がPerson
構造体であるという型情報は落ちてしまっているんですよね。
にも関わらずDbMap
構造体のメソッドAddTableWithName
でPerson
構造体と対応付けたperson
テーブル、Company
構造体と対応付けたcompany
テーブルにそれぞれレコードがインサートできています。
何故なんでしょうか。
この時、更にGoに慣れている人であれば「恐らくInsert
メソッドは内部でreflect
パッケージを利用しているのだろう」と考えるはずです。
reflect
パッケージはその名の通り所謂リフレクションを行うライブラリです。
ここでの文脈に則って言うと「interface{}
で受け取り型情報の落ちた変数から型情報を動的に取りだす」ことができます。
実際にInsert
メソッドの実装を見てみると以下のような処理が見つかります(一部抜粋)。
ptrv := reflect.ValueOf(ptr)
変数ptr
はInsert
メソッドに渡した引数です。
ここで得たptrv
に対して、ptrv.Type()
のようにType
メソッドを呼びだすことで型情報を取得できます。
ここまで分かればGo構造体とデータベーススキーマとの対応付けは既にあるわけですし、実際にインサートすることができそうですね。
reflect
Package Internals
reflect
パッケージを利用することでリフレクションを行い元々変数の持っていた型情報を取得できることが分かりました。
ここまで分かると次の疑問が湧いてきます。
「どうして元々の型情報が取得できるんだろう」
reflect
パッケージの中身を追ってみましょう。
reflect
パッケージはGoの標準パッケージの1つなのでソースコードはGo本体に含まれています。
VelueOf
関数
まずはValueOf
関数から見ていきたいですね。ValueOf
関数の実装はsrc/reflect/value.go
ファイルにあります。
とはいえValueOf
関数の中身はコメントを除くと5行しかなく、その内部で呼んでいるunpackEface
関数が実体ですのでこちらを見ていきます。
func unpackEface(i interface{}) Value { e := (*emptyInterface)(unsafe.Pointer(&i)) t := e.typ if t == nil { return Value{} } f := flag(t.Kind()) if ifaceIndir(t) { f |= flagIndir } return Value{t, e.word, f} }
unpackEface
の引数i
はValueOf
の引数がそのまま渡されています。
1行目でunsafe.Pointer
を使いi
のメモリ上のアドレスを取得した上で、emptyInterface
型のポインタに型変換しています。
emptyInterface
の定義を見ると次のようになっています。
type emptyInterface struct { typ *rtype word unsafe.Pointer }
typ
というrtype
型のフィールドがあります。名前的にも型情報に関係する何かであることが想像できますね。
rtype
の定義は以下のようになっています。
type rtype struct { size uintptr ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero }
なんとなく型情報を得るために必要そうなデータが詰まっている雰囲気がありますね。
ValueOf
関数の最後の行ではrytpe
型の変数t
をValue
型のフィールドに詰めて返しています。
ところでunpackEface
関数の冒頭の型変換ですがどうしてこんなことができるのでしょう。
引数i
は任意の型へのポインタですが、int32
等のプリミティブな型やPerson
構造体等の独自定義型を、共通の方法で強制的に型変換したところでemptyInterface
のフィールドtyp
やword
に望んだ通りの情報が収まるとは思えません。
そこで以下の様なプログラムを書いてみます。
package main func func1(y *Person) { } func func2(z interface{}) { } type Person struct { Name string Age int32 } func main() { x := Person{ Name: "Test", Age: 25, } func1(&x) func2(&x) }
Person
型の変数をそれぞれ関数func1
, func2
に渡すだけのプログラムです。
func1
では引数をPerson
型のポインタとして受け取り、一方func2
ではinterface{}
として受け取ります。
このプログラムをコンパイルしてできたバイナリファイルをgdb
を使いデバッグします。func1
, func2
に渡された時点での引数の値を見ていきましょう。
実行結果は以下の様になります。
$ go build -gcflags='-N -l' main.go $ gdb main (略. func1, func2のエントリポイントにブレイクポイントを設定する) $ p y (func1の引数をプリント) $1 = (main.Person *) 0x44f9e8 <main.main+88> $ c $ p z (func2の引数をプリント) $2 = { _type = 0x44fa07 <main.main+119>, data = 0x457360 }
func1
, func2
では同じ変数のポインタを渡したのにも関わらず、引数の構造が変わっていることが分かると思います。
func1
の引数y
はPerson
型へのポインタですが、func2
の引数z
は何らかの構造体となっておりポインタアドレスと値を持つことが分かります。
このことから引数をinterface{}
で受け取った場合は値の変換処理が行われていることが分かります。
また、この変数z
の構造ですが、よく見てみると上で見たemptyInterface
の構造と一致していることが分かります。
構造が一致しているため値のunsafe.Pointer
を取り型変換を行った時でもemptyInterface
のフィールドに望んだ通りの値が入ることになります。
引数の変換処理は受け取った値をemptyInterface
に適した構造へ変えるのものだと言うことも分かるかと思います。
Type
メソッド
続いてType
メソッドを見ていきましょう。
Type
メソッドのシグニチャは次のようになっています。
func (v Value) Type() Type
先程、ValueOf
関数の返り値がValue
型であることを見ていきましたが、Value
型はType
メソッドを持ち、このメソッドはType
型を返しています。
Type
型はインタフェースとして以下のように定義されています。
type Type interface { // 省略 Size() uintptr String() string // 省略 }
型サイズを返すSize
関数や、型の名前返すString
関数等が定義されていることが分かります。
rtype
について詳細に見ていくと分かるのですが、rtype
はこのType
インタフェースを実装しています。
実際にrtype.
型の変数に対してString
メソッドを呼んでみると*main.Person
というように型名が取得できることが分かります。
まとめ
gorp
パッケージ使用時に生じた疑問から始まりreflect
パッケージの中身を見てきました。
reflect
パッケージの内部では型情報はrtype
構造体で管理されており、そこからユーザフレンドリーな形で型情報を取りだすためのインタフェースがType
型で定義されていることが分かりました。
rtype
型のフィールドとして直接的に型情報をもつことはせず、rtype
型のフィールドは基本的にuintptr
やuint8
型等のメモリフットプリントが小さくなるような型で用意しているところも面白いですね。
今回は触れていませんが、Type
型が提供するメソッドの中ではこれらのフィールドが持つメモリアドレスを直接操作し型情報を頑張って取りだすような実装がされています。
We're hiring!
エムスリーではGoを自在に操るエンジニアを募集しています。社員とカジュアルにお話することもできますので、興味を持たれた方は下記よりお問い合わせください。