エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Goのreflectパッケージと型の内部表現について

エムスリーエンジニアリンググループの木村です。 普段はBIRという医療従事者の会員向けアンケートをベースに、製薬会社へのマーケティング支援を提供するチームでソフトウェアエンジニア兼チームSREをやっています。

入社とほぼ同時にGoを書き始め、そろそろ1年が経とうとしています。 主にWebアプリケーションのバックエンドの実装にGoを利用していますが、その他にも小さいCLIツール等もGoで書くことが多くなってきています。

今回は業務でWebアプリケーションを開発していくにあたって疑問が生じた点とその時行ったreflectパッケージの調査について書いていきます。

f:id:itto_ki:20171021190042j:plain
水面などに写った被写体を撮った写真をリフレクション写真と呼ぶことがあります。

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メソッドに引数として渡した時点で変数vPerson構造体であるという型情報は落ちてしまっているんですよね。 にも関わらずDbMap構造体のメソッドAddTableWithNamePerson構造体と対応付けたpersonテーブル、Company構造体と対応付けたcompanyテーブルにそれぞれレコードがインサートできています。 何故なんでしょうか。

この時、更にGoに慣れている人であれば「恐らくInsertメソッドは内部でreflectパッケージを利用しているのだろう」と考えるはずです。

reflectパッケージはその名の通り所謂リフレクションを行うライブラリです。 ここでの文脈に則って言うと「interface{}で受け取り型情報の落ちた変数から型情報を動的に取りだす」ことができます。

実際にInsertメソッドの実装を見てみると以下のような処理が見つかります(一部抜粋)。

ptrv := reflect.ValueOf(ptr)

変数ptrInsertメソッドに渡した引数です。

ここで得たptrvに対して、ptrv.Type()のようにTypeメソッドを呼びだすことで型情報を取得できます。

ここまで分かればGo構造体とデータベーススキーマとの対応付けは既にあるわけですし、実際にインサートすることができそうですね。

reflect Package Internals

reflectパッケージを利用することでリフレクションを行い元々変数の持っていた型情報を取得できることが分かりました。

ここまで分かると次の疑問が湧いてきます。

「どうして元々の型情報が取得できるんだろう」

reflectパッケージの中身を追ってみましょう。 reflectパッケージはGoの標準パッケージの1つなのでソースコードはGo本体に含まれています。

github.com

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の引数iValueOfの引数がそのまま渡されています。

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型の変数tValue型のフィールドに詰めて返しています。

ところでunpackEface関数の冒頭の型変換ですがどうしてこんなことができるのでしょう。 引数iは任意の型へのポインタですが、int32等のプリミティブな型やPerson構造体等の独自定義型を、共通の方法で強制的に型変換したところでemptyInterfaceのフィールドtypwordに望んだ通りの情報が収まるとは思えません。

そこで以下の様なプログラムを書いてみます。

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の引数yPerson型へのポインタですが、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型のフィールドは基本的にuintptruint8型等のメモリフットプリントが小さくなるような型で用意しているところも面白いですね。 今回は触れていませんが、Type型が提供するメソッドの中ではこれらのフィールドが持つメモリアドレスを直接操作し型情報を頑張って取りだすような実装がされています。

We're hiring!

エムスリーではGoを自在に操るエンジニアを募集しています。社員とカジュアルにお話することもできますので、興味を持たれた方は下記よりお問い合わせください。

open.talentio.com

jobs.m3.com