最新の高級言語のほとんどは、さまざまな形でリフレクションをサポートしています。リフレクションは諸刃の剣です。強力ではありますが、コードの可読性は理想的ではありません。必要な場合を除き、リフレクションを使用することはお勧めできません。
次に、Go言語におけるリフレクションの具体的な実施例と、リフレクションの基本的な使い方を紹介します。
リフレクションの基本概念
Go 言語は、実行時に変数の値を更新およびチェックし、変数のメソッドと変数によってサポートされる内部操作を呼び出すメカニズムを提供しますが、これらの変数の具体的な型はコンパイル時には不明であり、このメカニズムはリフレクションと呼ばれます。リフレクションを使用すると、型自体を第一級の値型として扱うこともできます。
リフレクションとは、プログラムの実行中にプログラム自体にアクセスして変更する機能を指します。プログラムがコンパイルされるときに、変数はメモリ アドレスに変換され、変数名はコンパイラによって実行可能部分に書き込まれません。プログラムの実行中にプログラム自体を取得できません。
リフレクションをサポートする言語は、プログラムのコンパイル時にフィールド名、型情報、構造情報などの変数リフレクション情報を実行可能ファイルに統合し、プログラムがリフレクション情報にアクセスするためのインターフェイスを提供できるため、プログラムの実行時に取得できるようになります。反射情報の種類とそれらを変更する機能。
C/ C++言語はリフレクション関数をサポートしておらず、typeid を通じて非常に弱いプログラム実行時の型情報しか提供できません。Java、 C#およびその他の言語は完全なリフレクション関数をサポートしています。Lua、 JavaScriptのような動的言語は、独自の文法的特性により、コードが実行時にプログラム自身の値と型情報にアクセスできるようにするため、リフレクション システムは必要ありません。
Go 言語プログラムのリフレクション システムは、実行可能ファイル空間またはパッケージ内のすべての型情報を取得することはできず、標準ライブラリ内の対応する字句、パーサー、および抽象構文ツリー (AST) を使用してソース コードをスキャンする必要があります。この情報を入手してください。
Go 言語は、プログラムのリフレクション情報にアクセスするためのリフレクト パッケージを提供します。
パッケージを反映する
Go 言語のリフレクションは、2 つの重要な型 Type と Value を定義するリフレクト パッケージによってサポートされています。リフレクションのインターフェイス値は、reflect.Type とreflect.Value で構成されるものとして理解でき、reflect パッケージは、reflect.TypeOf と 2 つの関数を提供します。任意のオブジェクトの値と型を取得するために、reflect.ValueOf が追加されます。
反射型オブジェクト (reflect.Type)
Go 言語プログラムでは、reflect.TypeOf() 関数を使用して、任意の値の型オブジェクト (reflect.Type) を取得できます。プログラムは、型オブジェクトを通じて任意の値の型情報にアクセスできます。以下は例です。型オブジェクトを取得するプロセスを理解するには、次のようにします。
package main
import (
"fmt"
"reflect"
)
func main() {
var a int
typeOfA := reflect.TypeOf(a)
fmt.Println(typeOfA.Name(), typeOfA.Kind())
}
操作の結果は次のようになります。
int int
コードの説明は次のとおりです。
- 9 行目では、int 型の変数を定義しています。
- 10行目では、変数aの型オブジェクトtypeOfAをreflect.TypeOf()で取得しており、型はreflect.Type()となっています。
- 11行目では、typeOfA型オブジェクトのメンバ関数により、typeOfA変数の型名がint、種類(Kind)がintとなっています。
反射タイプ (Type) と種類 (Kind)
リフレクションを使用する場合は、まず Type と Kind の違いを理解する必要があります。プログラミングでは型が最もよく使われますが、反省すると多種多様な型を区別する必要がある場合には Kind が使用されます。例えば、型内のポインタを一律に判定する必要がある場合には、型(Kind)情報を利用する方が便利である。
1) 反射タイプの定義(種類)
Go 言語プログラムの型 (Type) は、int、string、bool、float32 などのシステムのネイティブ データ型、および type キーワードを使用して定義された型を指します。これらの型の名前は型自体の名前。たとえば、タイプ A struct{} を使用して構造体を定義する場合、A は struct{} のタイプになります。
Kind (Kind) は属するオブジェクトのタイプを指し、reflect パッケージで次のように定義されます。
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 // アンセーフポインター
)
Map、Slice、および Chan は参照型であり、使用されるポインターに似ていますが、型定数の定義では Ptr ではなく独立した型に属します。型 A struct{} で定義された構造体は Struct 型に属し、*A は Ptr に属します。
2) 型オブジェクトから型名と種類を取得します。
Go言語における型名に対応するリフレクション取得メソッドは、型名を表す文字列を返すreflect.TypeのName()メソッドであり、型の種類(Kind)はreflectのKind()メソッドに属します。 Type、reflect.Kind 型の定数を返します。
次のコードは、定数と構造体の型情報を取得します。
package main
import (
"fmt"
"reflect"
)
// Enum型を定義する
type Enum int
const (
Zero Enum = 0
)
func main() {
// 空の構造体の型を宣言する
type cat struct {
}
// 構造体のインスタンスのリフレクション型オブジェクトを取得する
typeOfCat := reflect.TypeOf(cat{})
// リフレクション型オブジェクトの名前と種類を表示する
fmt.Println(typeOfCat.Name(), typeOfCat.Kind())
// Zero定数のリフレクション型オブジェクトを取得する
typeOfA := reflect.TypeOf(Zero)
// リフレクション型オブジェクトの名前と種類を表示する
fmt.Println(typeOfA.Name(), typeOfA.Kind())
}
操作の結果は次のようになります。
cat struct
Enum int
コードの説明は次のとおりです。
- 17 行目では、構造体タイプ cat を宣言しています。
- 20行目でcatをインスタンス化し、reflect.TypeOf()を使ってインスタンス化されたcatのリフレクション型オブジェクトを取得しています。
- 22行目はcatの型名と型を出力していますが、型名はcat、catは構造体型に属しているので型はstructになります。
- 24 行目で、Zero は Enum 型の定数です。 Enum 型は 9 行目で宣言され、定数は 12 行目で宣言されます。定数がなくインスタンスが作成できない場合は、reflect.TypeOf()によりリフレクション型オブジェクトを直接取得してください。
- 26行目では、Zeroに対応する型オブジェクトの型名と型を出力します。
ポインタとポインタが指す要素
Go 言語プログラムでポインターのリフレクション オブジェクトを取得する場合、reflect.Elem() メソッドを通じてポインターが指す要素の型を取得できます。この取得プロセスは要素のフェッチと呼ばれ、 *
を実行するのと同等です。ポインタ型変数に対する演算。コードは次のとおりです。
パッケージ main
インポート (
"fmt"
"reflect"
)
func main() {
// 空構造体を宣言する
type cat struct {
}
// catのインスタンスを作成する
ins :=&cat{}
// 構造体インスタンスの反射型オブジェクトを取得する
typeOfCat := reflect.TypeOf(ins)
// 反射型オブジェクトの名前と種類を表示する
fmt.Printf("name: '%v' kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
// タイプ要素を取得する
typeOfCat = typeOfCat.Elem()
// 反射型オブジェクトの名前と種類を表示する
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}
操作の結果は次のようになります。
name:” kind:’ptr’
element name: ‘cat’, element kind: ‘struct’
コードの説明は次のとおりです。
- 13 行目は cat 構造体のインスタンスを作成します。ins は *cat 型のポインター変数です。
- 15行目でポインタ変数のリフレクション型情報を取得しています。
- 17行目では型名とポインタ変数の型を出力しています。 Go 言語を反映して、すべてのポインター変数の型は Ptr ですが、ポインター変数の型名は *cat ではなく空であることに注意してください。
- 19行目ではポインタ型の要素型、つまりcat型を取得しています。この操作は元に戻すことができず、そのポインター型を非ポインター型から取得することはできません。
- 21行目では出力ポインタ変数が型名と要素の型を指しており、catの型名(cat)と型(struct)を取得しています。
リフレクションを使用して構造体のメンバー型を取得する
任意の値がreflect.TypeOf()を通じてリフレクションオブジェクトの情報を取得した後、その型が構造体の場合、リフレクション値オブジェクトreflect.TypeのNumField()およびField()メソッドを通じて構造体のメンバーの詳細情報を取得できます。 。
メンバー取得に関するreflect.Typeのメソッドを以下の表に示します。
方法 | 説明する |
---|---|
Field(i int) StructField | インデックスに従い、インデックスに対応する構造体フィールドの情報を返します 値が構造体でない場合やインデックスが範囲外の場合はクラッシュします |
NumField() int | 構造体のメンバー フィールドの数を返します。型が構造体でない場合、またはインデックスが範囲外の場合、クラッシュが発生します。 |
FieldByName(name string) (StructField, bool) | 指定された文字列に従って、その文字列に対応する構造体フィールドの情報を返します。見つからない場合、bool は false を返します。型が構造体でない場合、またはインデックスが範囲外の場合、クラッシュが発生します |
FieldByIndex(index []int) StructField | 多層メンバーにアクセスする場合、[]int で提供される各構造体のフィールド インデックスに従ってフィールド情報を返し、見つからない場合は 0 の値を返します。型が構造体でない場合、またはインデックスが範囲外の場合にクラッシュする |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 照合関数に従って必須フィールドを照合します。値が構造体でない場合、またはインデックスが範囲外の場合にクラッシュが発生します。 |
1) 構造体フィールドの種類
Reflect.Type の Field() メソッドは、構造体のメンバー情報を記述した StructField 構造体を返します。この情報を通じて、オフセット、インデックス、匿名かどうかなど、メンバーと構造体の関係を取得できます。フィールド、構造体タグ(StructTag)等があり、さらにStructFieldのTypeフィールドを通じて構造体メンバの型情報を取得することができます。
StructField の構造は次のとおりです。
type StructField struct {
Name string // フィールド名
PkgPath string // フィールドのパス
Type Type // フィールドの反射型オブジェクト
Tag StructTag // フィールドの構造体タグ
Offset uintptr // 構造体内でのフィールドの相対オフセット
Index []int // Type.FieldByIndexが返すインデックス値
Anonymous bool // 匿名フィールドかどうか
}
フィールドは次のように説明されます。
- 名前: はフィールド名です。
- PkgPath: 構造内のフィールドのパス。
- Type: フィールド自体のリフレクション型オブジェクト。型はreflect.Typeで、さらにフィールドの型情報を取得できます。
- タグ: 構造体タグ。構造体フィールドタグの追加情報であり、個別に抽出できます。
- Index: FieldByIndex のインデックス順序。
- 匿名: フィールドが匿名かどうかを示します。
2) メンバー反映情報の取得
次のコードでは、構造体をインスタンス化し、その構造体メンバーを走査し、reflect.Type の FieldByName() メソッドを使用して構造体内で指定された名前を持つフィールドを検索し、その型情報を直接取得します。
構造体のメンバーのタイプと情報への反射型アクセス:
パッケージ main
import (
"fmt"
"reflect"
)
func main() {
// 空の構造体を宣言する
type cat struct {
Name string
// 構造体タグを持つフィールド
Type int `json:"type" id:"100"`
}
// catのインスタンスを作成する
ins := cat{Name: "mimi", Type: 1}
// 構造体インスタンスのリフレクションタイプオブジェクトを取得する
typeOfCat := reflect.TypeOf(ins)
// 構造体のすべてのメンバーを反復処理する
for i := 0; i < typeOfCat.NumField(); i++ {
// 各メンバーの構造体フィールドタイプを取得する
fieldType := typeOfCat.Field(i)
// メンバー名とタグを出力する
fmt.Printf("name: %v tag: '%v'\n", fieldType.Name, fieldType.Tag)
}
// フィールド名からフィールドタイプ情報を見つける
if catType, ok := typeOfCat.FieldByName("Type"); ok {
// tagから必要なtagを取り出す
fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
}
}
コード出力は次のとおりです。
name: Name tag: ”
name: Type tag: ‘json:”type” id:”100″‘
type 100
コードの説明は次のとおりです。
- 10 行目では、2 つのメンバーを持つ cat 構造体が宣言されています。
- 13 行目では、Type は cat のメンバーであり、このメンバーの型の後に ` で始まり ` で終わる文字列が続きます。この文字列をGo言語ではタグと呼びます。これは通常、フィールドにカスタム情報を追加するために使用され、他のモジュールがその情報に従ってさまざまな機能を処理できるようになります。
- 16 行目では cat インスタンスを作成し、2 つのフィールドに値を割り当てます。構造タグは型情報に属し、必須ではないため割り当てることはできません。
- 18行目、インスタンスのリフレクション型オブジェクトを取得します。
- 20 行目では、reflect.Type 型の NumField() メソッドを使用して、構造体型に含まれるフィールドの数を取得します。型が構造体ではない場合、クラッシュ エラーがトリガーされます。
- 22 行目では、reflect.Type の Field() メソッドと NumField は、通常、構造体のメンバーの走査操作を実装するためにペアで使用されます。
- 24 行目で、reflect.Type の Field() メソッドによって返される構造体は、reflect.Type ではなく、StructField 構造体になりました。
- 27 行目では、reflect.Type の FieldByName() を使用して、フィールド名に従って構造体フィールド情報を検索します。catType は返された構造体フィールド情報を示し、型は StructField で、ok は構造体フィールド情報が見つかったかどうかを示します。
- 29行目では、StructFieldのTagのGet()メソッドを使用して、Tagの名前に応じた情報を取得します。
構造体のメンバ情報をreflect.Typeで取得するreflect.StructField構造体のタグを構造体タグ(StructTag)と呼びます。構造体タグは、構造体フィールドの追加情報タグです。
JSON や BSON などの形式はシリアル化されており、オブジェクト リレーショナル マッピング (ORM) システムは構造タグを使用します。これらのシステムはタグを使用して、処理中にフィールドに必要な特別なプロパティと可能な動作を設定します。これらの情報は静的であり、構造をインスタンス化せずにリフレクションを通じて取得できます。
1) 構造体タグのフォーマット
構造体フィールドの後ろに記述されるタグの形式は次のとおりです。
`key1:”value1″ key2:”value2″`
構造体タグは 1 つ以上のキーと値のペアで構成されます。キーと値はコロンで区切られ、値は二重引用符で囲まれます。キーと値のペアはスペースで区切られます。
2) 構造体タグから値を取得する
StructTag には、次のように、タグ情報を解析して抽出するためのメソッドがいくつかあります。
func (tag StructTag) Get(key string) string
: タグ内のキーに従って対応する値を取得します。たとえば、`key1:"value1" key2:"value2"`
のタグでは、「key1」を渡すことができます。 ” を使用して “value1” を取得します。func (tag StructTag) Lookup(key string) (value string, ok bool)
: Tag のキーに従って、値が存在するかどうかをクエリします。
3) 構造タグの形式が間違っていることによる問題
タグを作成するときは、キーと値のペアの規則に厳密に従う必要があります。構造体タグの解析コードのエラー許容度は非常に低く、一度フォーマットが間違って記述されても、コンパイル時や実行時にエラーは表示されません。サンプル コードは次のとおりです。
package main
import (
"fmt"
"reflect"
)
func main() {
type cat struct {
Name string
Type int `json:"type" id:"100"`
}
typeOfCat := reflect.TypeOf(cat{})
if catType, ok := typeOfCat.FieldByName("Type"); ok {
fmt.Println(catType.Tag.Get("json"))
}
}
上記のコードを実行すると、期待される型ではなく空の文字列が出力されます。
コードの11行目では、json:とtypeの間にスペースが入っていますが、この書き方では構造体タグのルールに従わないため、Tag.Getでは正しいjson対応値を取得できません。このエラーは開発中に非常に見落とされやすく、その結果、目に見えないエラーが発生します。そこで、12行目のコードを以下のように変更すると正常に印刷できるようになります。
type cat struct {
Name string
Type int `json:"type" id:"100"`
}
操作の結果は次のようになります。
type