Go上級 #4 reflect パッケージ — ランタイムに型を扱う

#3 ジェネリクスの次、今回は別の毛色の動的ツール。reflect

reflectは — ランタイムに型を覗き込んで操作するツールです。直接使う機会は多くありませんが — encoding/jsontext/template、ORMのようなライブラリの内側で常に見えます。どう動作するかを知れば、それらのライブラリが自然に見えてきます。

基本2概念 — Type と Value #

reflect 入門
import "reflect"

func main() {
	x := 42

	t := reflect.TypeOf(x)
	v := reflect.ValueOf(x)

	fmt.Println(t)         // int
	fmt.Println(v)         // 42
	fmt.Println(t.Kind())  // int
}
  • reflect.Type — 型のメタデータ(名前、kind、フィールド、メソッドなど)
  • reflect.Value — 値そのものと型情報の両方

Kind — 型の大分類 #

v := reflect.ValueOf(struct{ X int }{42})
fmt.Println(v.Kind())    // struct

Kindは — より大きな分類(int、string、struct、slice、map、ptrなど)。Typeはもっと具体的(例: mypkg.Userという定義された型まで)。

struct のフィールドを覗く #

struct フィールド走査
type User struct {
	Name  string
	Age   int
	Email string
}

u := User{"イ・ドギョン", 30, "x@y.z"}
v := reflect.ValueOf(u)
t := v.Type()

for i := 0; i < t.NumField(); i++ {
	field := t.Field(i)
	value := v.Field(i)
	fmt.Printf("%s %v = %v\n", field.Name, field.Type, value)
}
// Name string = イ・ドギョン
// Age int = 30
// Email string = x@y.z

t.NumField()t.Field(i)v.Field(i) — JSONエンコーダが内部でやることと正確に同じです。

struct タグを読む #

struct タグ
type User struct {
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
}

t := reflect.TypeOf(User{})
field := t.Field(0)
fmt.Println(field.Tag.Get("json"))    // name

Field(i).Tag.Get("json")json:"..."の部分だけ取り出します。encoding/jsonはこの情報でどうシリアライズするか決定します。

このメカニズムが — Goライブラリがstructだけを見て多様な動作を可能にする中核。

値の修正 — Value が settable でなければならない #

値の修正
x := 42
v := reflect.ValueOf(&x).Elem()    // ポインタ → 指す値
v.SetInt(100)
fmt.Println(x)                      // 100

中核は2つ:

  • ポインタでValueOfして — Elem()で指す値を得る
  • その値がsettableでなければSetIntSetStringなどができない
settable 検証
x := 42
v := reflect.ValueOf(x)         // 値コピー
v.CanSet()                       // false — コピーなので修正不可

v = reflect.ValueOf(&x).Elem()
v.CanSet()                       // true

動的な関数呼び出し #

reflect で関数呼び出し
func add(a, b int) int { return a + b }

f := reflect.ValueOf(add)
results := f.Call([]reflect.Value{
	reflect.ValueOf(1),
	reflect.ValueOf(2),
})
fmt.Println(results[0].Int())    // 3

Callが — ランタイムに関数を呼び出せるようにしてくれます。RPCライブラリ、ハンドラディスパッチのような場面で活用。

インターフェース変換 — Type assertionの動的版 #

reflect と interface
var i interface{} = "hello"

v := reflect.ValueOf(i)
fmt.Println(v.Kind())     // string
fmt.Println(v.String())   // hello

interface{}の中に何が入っているかを — 型アサーションなしに検査できます。

典型的な使用例 — JSON エンコーディングのミニ版 #

理解するとreflectの位置づけが明確になります。数行で作ってみます。

シンプルなシリアライズ
func toMap(v any) map[string]any {
	rv := reflect.ValueOf(v)
	rt := rv.Type()

	m := map[string]any{}
	for i := 0; i < rt.NumField(); i++ {
		field := rt.Field(i)
		key := field.Tag.Get("json")
		if key == "" {
			key = field.Name
		}
		m[key] = rv.Field(i).Interface()
	}
	return m
}

type User struct {
	Name string `json:"name"`
	Age  int
}

toMap(User{"イ・ドギョン", 30})
// map[name:イ・ドギョン Age:30]

encoding/jsonも本質的に同じことを — もっと精巧にやります(zero value処理、omitempty、ネストした構造体、スライスなど)。

よく出会う場面 — 環境変数の自動マッピング #

env 自動マッピング
type Config struct {
	Port int    `env:"PORT"`
	Host string `env:"HOST"`
}

func loadEnv(cfg any) {
	v := reflect.ValueOf(cfg).Elem()
	t := v.Type()

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		key := field.Tag.Get("env")
		val := os.Getenv(key)
		if val == "" {
			continue
		}

		fv := v.Field(i)
		switch fv.Kind() {
		case reflect.String:
			fv.SetString(val)
		case reflect.Int:
			n, _ := strconv.Atoi(val)
			fv.SetInt(int64(n))
		}
	}
}

このパターンが — caarlos0/envkelseyhightower/envconfigのようなライブラリの中核。

罠 — reflect は遅い #

reflectは通常のコードより — 数十倍遅いです。理由:

  • 型検査をランタイムに行う
  • メモリ割り当てがより多い
  • コンパイラが最適化できない
benchmark 例
BenchmarkDirect-8       1000000000      0.3 ns/op
BenchmarkReflect-8        50000000       30 ns/op

ホットパスでは — reflectの代わりにコード生成(次の#7)を検討してください。またはinterface + type switchがreflectより速い場面も多いです。

罠 — コンパイラの助けがなくなる #

v.SetString("hi")    // もし v が int なら → ランタイム panic

reflectは静的型チェックをバイパスします。誤用するとコンパイル時ではなく — ランタイムpanic。常にKind()で検査してから入っていくのが安全。

罠 — interface{} の特殊な nil #

よく出会う罠
var p *User = nil
var i interface{} = p

i == nil    // false — interface は (型、値) のペア
v := reflect.ValueOf(i)
v.IsNil()   // true — Value の IsNil は内側の値を見る

interfaceがnilであるためには型と値の両方がnilでなければなりません。reflect.Value.IsNil()が — 内側のポインタのnilかどうかを教えてくれます(Kindに応じて)。

unsafe.Pointerとの関係 #

reflectは — 内部的にunsafe.Pointerを活用します。だから安全ですが — 誤用すれば同じリスクを生み出せます。通常はライブラリ作成者だけが深く入り、一般ユーザーは表面APIだけを使います。

#5 unsafe と cgoでunsafeを扱います。

いつ使うべきか? — 回避できれば回避 #

reflectの原則:

  • コンパイル時情報で十分なら → ジェネリクス、インターフェース、コード生成
  • 型を知らなくても動作しなければならないライブラリ → reflectが適切

ほとんどのビジネスロジックは — 1つ目に該当。reflectを直接使う機会はライブラリ/フレームワークコードでより一般的です。

“Reflection is never clear.” — Go proverbs

実戦 — fmt.Printlnもreflectのユーザー #

fmt.Printf("%v", x)が — 任意の型を受け取って出力できる理由はreflect。標準ライブラリにおける代表的な使用場面です。

fmt.Sprintf("%v", User{"イ・ドギョン", 30})    // {イ・ドギョン 30}

まとめ #

今回の記事で整理した内容:

  • reflect.Type — 型メタデータ、reflect.Value — 値 + 型
  • Kind — 型の大分類(int、struct、ptrなど)
  • structフィールド/タグ走査NumFieldFieldTag.Get
  • 値の修正 — ポインタ → Elem() → settableか確認 → Set*
  • 動的呼び出しreflect.Value.Call
  • 遅い — ホットパス回避、ホットならコード生成で代替
  • 静的型チェックバイパス — ランタイムpanicリスク
  • 可能ならジェネリクス/インターフェースを優先

次の記事(#5 unsafe と cgo)では — Goの安全領域の外へ一歩出る2つのツールを整理します。ほぼ使いませんが、どんな場面がそのツールを呼ぶのかは知っておく価値があります。

X