title: 【転載】Go 言語基礎のリフレクション
date: 2021-08-09 16:40:33
comment: false
toc: true
category:
- Golang
tags: - 転載
- Go
- 基礎
- リフレクション
この記事は以下から転載されています:Go 言語基礎のリフレクション | 李文周のブログ
この記事では Go 言語のリフレクションの意義と基本的な使い方について紹介します。
変数の内在メカニズム#
Go 言語の変数は二つの部分に分かれています:
- 型情報:あらかじめ定義されたメタ情報。
- 値情報:プログラム実行中に動的に変化するもの。
リフレクションの紹介#
リフレクションとは、プログラムの実行時にプログラム自体にアクセスし、変更する能力を指します。プログラムがコンパイルされると、変数はメモリアドレスに変換され、変数名はコンパイラによって実行可能部分に書き込まれません。プログラムを実行する際、プログラムは自分自身の情報を取得できません。
リフレクションをサポートする言語は、プログラムのコンパイル時に変数のリフレクション情報(フィールド名、型情報、構造体情報など)を実行可能ファイルに統合し、プログラムにリフレクション情報にアクセスするインターフェースを提供します。これにより、プログラムの実行時に型のリフレクション情報を取得し、それらを変更する能力を持つことができます。
Go プログラムは実行時に reflect パッケージを使用してプログラムのリフレクション情報にアクセスします。
前回のブログでは空インターフェースについて紹介しました。空インターフェースは任意の型の変数を格納できますが、空インターフェースが保存しているデータが何であるかをどうやって知るのでしょうか?リフレクションは、実行時に変数の型情報と値情報を動的に取得することです。
reflect パッケージ#
Go 言語のリフレクションメカニズムでは、任意のインターフェース値は具体的な型
と具体的な型の値
の二つの部分で構成されています(前回のインターフェースに関するブログで関連する概念を紹介しました)。Go 言語におけるリフレクションの関連機能は組み込みの reflect パッケージによって提供され、任意のインターフェース値はリフレクションにおいてreflect.Type
とreflect.Value
の二つの部分から成り立ち、reflect パッケージはreflect.TypeOf
とreflect.ValueOf
の二つの関数を提供して任意のオブジェクトの Value と Type を取得します。
TypeOf#
Go 言語では、reflect.TypeOf()
関数を使用して任意の値の型オブジェクト(reflect.Type)を取得できます。プログラムは型オブジェクトを通じて任意の値の型情報にアクセスできます。
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("type:%v\n", v)
}
func main() {
var a float32 = 3.14
reflectType(a) // type:float32
var b int64 = 100
reflectType(b) // type:int64
}
type name と type kind#
リフレクションにおいて、型はさらに二つに分類されます:型(Type)
と種別(Kind)
。Go 言語では type キーワードを使用して多くのカスタム型を構築できますが、種別(Kind)
は基礎的な型を指します。しかしリフレクションでは、ポインタや構造体などの大きな種類の型を区別する必要がある場合に種別(Kind)
が使用されます。例えば、二つのポインタ型と二つの構造体型を定義し、リフレクションを通じてそれらの型と種別を確認します。
package main
import (
"fmt"
"reflect"
)
type myInt int64
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
var a *float32 // ポインタ
var b myInt // カスタム型
var c rune // 型エイリアス
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "沙河小王子",
age: 18,
}
var e = book{title: "《小王子からGo言語を学ぶ》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}
Go 言語のリフレクションにおいて、配列、スライス、マップ、ポインタなどの型の変数は、その.Name()
が空
を返します。
reflect
パッケージで定義された Kind 型は以下の通りです:
type Kind uint
const (
Invalid Kind = iota // 無効な型
Bool // ブール型
Int // 符号付き整数型
Int8 // 符号付き8ビット整数型
Int16 // 符号付き16ビット整数型
Int32 // 符号付き32ビット整数型
Int64 // 符号付き64ビット整数型
Uint // 符号なし整数型
Uint8 // 符号なし8ビット整数型
Uint16 // 符号なし16ビット整数型
Uint32 // 符号なし32ビット整数型
Uint64 // 符号なし64ビット整数型
Uintptr // ポインタ
Float32 // 単精度浮動小数点数
Float64 // 倍精度浮動小数点数
Complex64 // 64ビット複素数型
Complex128 // 128ビット複素数型
Array // 配列
Chan // チャンネル
Func // 関数
Interface // インターフェース
Map // マップ
Ptr // ポインタ
Slice // スライス
String // 文字列
Struct // 構造体
UnsafePointer // 基底ポインタ
)
ValueOf#
reflect.ValueOf()
はreflect.Value
型を返し、その中には元の値の値情報が含まれています。reflect.Value
と元の値の間は相互に変換可能です。
reflect.Value
型が提供する元の値を取得するメソッドは以下の通りです:
メソッド | 説明 |
---|---|
Interface() interface {} | 値を interface {} 型で返し、型アサーションを通じて指定された型に変換可能 |
Int() int64 | 値を int 型で返し、すべての符号付き整数型はこの方法で返すことができます |
Uint() uint64 | 値を uint 型で返し、すべての符号なし整数型はこの方法で返すことができます |
Float() float64 | 値を倍精度(float64)型で返し、すべての浮動小数点数(float32、float64)はこの方法で返すことができます |
Bool() bool | 値を bool 型で返します |
Bytes() []bytes | 値をバイト配列 [] bytes 型で返します |
String() string | 値を文字列型で返します |
リフレクションを通じて値を取得する#
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()からリフレクションで整数型の元の値を取得し、int64()で強制的に型変換
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()からリフレクションで浮動小数点型の元の値を取得し、float32()で強制的に型変換
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()からリフレクションで浮動小数点型の元の値を取得し、float64()で強制的に型変換
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// int型の元の値をreflect.Value型に変換
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
リフレクションを通じて変数の値を設定する#
関数内でリフレクションを使用して変数の値を変更するには、関数の引数として値のコピーが渡されるため、変数のアドレスを渡す必要があります。リフレクションでは、専用のElem()
メソッドを使用してポインタに対応する値を取得します。
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) // 変更されるのはコピーであり、reflectパッケージはpanicを引き起こします
}
}
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// リフレクションではElem()メソッドを使用してポインタに対応する値を取得
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}
isNil () と isValid ()#
isNil()#
func (v Value) IsNil() bool
IsNil()
は v が保持する値が nil かどうかを報告します。v が保持する値の分類は、チャネル、関数、インターフェース、マップ、ポインタ、スライスのいずれかでなければなりません;そうでなければ IsNil 関数は panic を引き起こします。
isValid()#
func (v Value) IsValid() bool
IsValid()
は v が値を保持しているかどうかを返します。もし v が Value のゼロ値であれば偽を返し、この時 v は IsValid、String、Kind 以外のメソッドはすべて panic を引き起こします。
例を挙げると#
IsNil()
はポインタが空かどうかを判断するためによく使用されます;IsValid()
は返り値が有効かどうかを判定するためによく使用されます。
func main() {
// *int型の空ポインタ
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil値
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 匿名構造体をインスタンス化
b := struct{}{}
// 構造体から"abc"フィールドを探そうとする
fmt.Println("存在しない構造体メンバー:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 構造体から"abc"メソッドを探そうとする
fmt.Println("存在しない構造体メソッド:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// マップ
c := map[string]int{}
// マップから存在しないキーを探そうとする
fmt.Println("map中存在しないキー:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}
構造体リフレクション#
構造体に関連するメソッド#
任意の値はreflect.TypeOf()
を使用してリフレクションオブジェクト情報を取得した後、その型が構造体であれば、リフレクション値オブジェクト(reflect.Type
)のNumField()
とField()
メソッドを使用して構造体メンバーの詳細情報を取得できます。
reflect.Type
における構造体メンバー取得に関連するメソッドは以下の表の通りです。
メソッド | 説明 |
---|---|
Field(i int) StructField | インデックスに基づいて、インデックスに対応する構造体フィールドの情報を返します。 |
NumField() int | 構造体メンバーのフィールド数を返します。 |
FieldByName(name string) (StructField, bool) | 指定された文字列に基づいて、文字列に対応する構造体フィールドの情報を返します。 |
FieldByIndex(index []int) StructField | 多層メンバーアクセス時に、[] int で提供された各構造体のフィールドインデックスに基づいてフィールドの情報を返します。 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 渡されたマッチ関数に基づいて必要なフィールドをマッチングします。 |
NumMethod() int | この型のメソッド集に含まれるメソッドの数を返します |
Method(int) Method | この型のメソッド集に含まれる第 i のメソッドを返します |
MethodByName(string)(Method, bool) | メソッド名に基づいてこの型のメソッド集に含まれるメソッドを返します |
StructField 型#
StructField
型は構造体内の一つのフィールドの情報を記述するために使用されます。
StructField
の定義は以下の通りです:
type StructField struct {
// Nameはフィールドの名前です。PkgPathは非エクスポートフィールドのパッケージパスで、エクスポートフィールドの場合はこのフィールドは""です。
// 参照:http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // フィールドの型
Tag StructTag // フィールドのタグ
Offset uintptr // フィールドが構造体内でのバイトオフセット
Index []int // Type.FieldByIndex時のインデックススライス
Anonymous bool // 匿名フィールドかどうか
}
構造体リフレクションの例#
リフレクションを使用して構造体データを取得した後、インデックスを通じてフィールド情報を順に取得することも、フィールド名を通じて指定されたフィールド情報を取得することもできます。
type student struct {
Name string `json:"name"`
Score int `json:"score"`
}
func main() {
stu1 := student{
Name: "小王子",
Score: 90,
}
t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// forループを使用して構造体のすべてのフィールド情報を反復処理
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
// フィールド名を通じて指定された構造体フィールド情報を取得
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}
次に、printMethod(s interface{})
関数を作成して、s が含むメソッドを反復して印刷します。
// studentに二つのメソッドStudyとSleepを追加(注意:大文字で始まる)
func (s student) Study() string {
msg := "しっかり勉強して、毎日向上しましょう。"
fmt.Println(msg)
return msg
}
func (s student) Sleep() string {
msg := "しっかり寝て、早く成長しましょう。"
fmt.Println(msg)
return msg
}
func printMethod(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t.NumMethod())
for i := 0; i < v.NumMethod(); i++ {
methodType := v.Method(i).Type()
fmt.Printf("method name:%s\n", t.Method(i).Name)
fmt.Printf("method:%s\n", methodType)
// リフレクションを通じてメソッドを呼び出す際、引数は[]reflect.Value型で渡す必要があります
var args = []reflect.Value{}
v.Method(i).Call(args)
}
}
リフレクションは両刃の剣#
リフレクションは強力で表現力豊かなツールであり、より柔軟なコードを書くことを可能にします。しかし、リフレクションは乱用すべきではありません。その理由は以下の三つです。
- リフレクションに基づくコードは非常に脆弱であり、リフレクション内の型エラーは実際に実行されるまで panic を引き起こさないため、コードが書かれてから長い時間が経過してから発生する可能性があります。
- 大量のリフレクションを使用したコードは通常理解しにくいです。
- リフレクションの性能は低下し、リフレクションに基づいて実装されたコードは通常、通常のコードよりも 1〜2 桁遅く実行されます。
練習問題#
- リフレクションを利用して ini ファイルの解析器プログラムを実装するコードを書いてください。