Go上級 #4 reflect パッケージ — ランタイムに型を扱う
#3 ジェネリクスの次、今回は別の毛色の動的ツール。reflect。
reflectは — ランタイムに型を覗き込んで操作するツールです。直接使う機会は多くありませんが — encoding/json、text/template、ORMのようなライブラリの内側で常に見えます。どう動作するかを知れば、それらのライブラリが自然に見えてきます。
基本2概念 — Type と Value #
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()) // structKindは — より大きな分類(int、string、struct、slice、map、ptrなど)。Typeはもっと具体的(例: mypkg.Userという定義された型まで)。
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.zt.NumField()、t.Field(i)、v.Field(i) — JSONエンコーダが内部でやることと正確に同じです。
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")) // nameField(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でなければ
SetInt、SetStringなどができない
x := 42
v := reflect.ValueOf(x) // 値コピー
v.CanSet() // false — コピーなので修正不可
v = reflect.ValueOf(&x).Elem()
v.CanSet() // true動的な関数呼び出し #
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()) // 3Callが — ランタイムに関数を呼び出せるようにしてくれます。RPCライブラリ、ハンドラディスパッチのような場面で活用。
インターフェース変換 — Type assertionの動的版 #
var i interface{} = "hello"
v := reflect.ValueOf(i)
fmt.Println(v.Kind()) // string
fmt.Println(v.String()) // hellointerface{}の中に何が入っているかを — 型アサーションなしに検査できます。
典型的な使用例 — 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、ネストした構造体、スライスなど)。
よく出会う場面 — 環境変数の自動マッピング #
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/env、kelseyhightower/envconfigのようなライブラリの中核。
罠 — reflect は遅い #
reflectは通常のコードより — 数十倍遅いです。理由:
- 型検査をランタイムに行う
- メモリ割り当てがより多い
- コンパイラが最適化できない
BenchmarkDirect-8 1000000000 0.3 ns/op
BenchmarkReflect-8 50000000 30 ns/opホットパスでは — reflectの代わりにコード生成(次の#7)を検討してください。またはinterface + type switchがreflectより速い場面も多いです。
罠 — コンパイラの助けがなくなる #
v.SetString("hi") // もし v が int なら → ランタイム panicreflectは静的型チェックをバイパスします。誤用するとコンパイル時ではなく — ランタイム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フィールド/タグ走査 —
NumField、Field、Tag.Get - 値の修正 — ポインタ →
Elem()→ settableか確認 →Set* - 動的呼び出し —
reflect.Value.Call - 遅い — ホットパス回避、ホットならコード生成で代替
- 静的型チェックバイパス — ランタイムpanicリスク
- 可能ならジェネリクス/インターフェースを優先
次の記事(#5 unsafe と cgo)では — Goの安全領域の外へ一歩出る2つのツールを整理します。ほぼ使いませんが、どんな場面がそのツールを呼ぶのかは知っておく価値があります。