banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【転載】Go言語基礎のリフレクション

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.Typereflect.Valueの二つの部分から成り立ち、reflect パッケージはreflect.TypeOfreflect.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)
	}
}

リフレクションは両刃の剣#

リフレクションは強力で表現力豊かなツールであり、より柔軟なコードを書くことを可能にします。しかし、リフレクションは乱用すべきではありません。その理由は以下の三つです。

  1. リフレクションに基づくコードは非常に脆弱であり、リフレクション内の型エラーは実際に実行されるまで panic を引き起こさないため、コードが書かれてから長い時間が経過してから発生する可能性があります。
  2. 大量のリフレクションを使用したコードは通常理解しにくいです。
  3. リフレクションの性能は低下し、リフレクションに基づいて実装されたコードは通常、通常のコードよりも 1〜2 桁遅く実行されます。

練習問題#

  1. リフレクションを利用して ini ファイルの解析器プログラムを実装するコードを書いてください。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。