Go 言語ではリフレクションを使用して、コンパイル時に型を知らなくても変数を更新し、実行時に値を表示し、メソッドを呼び出し、レイアウトを直接操作します。
リフレクションは型システムに基づいて構築されているため、最初に Go 言語の型を確認しましょう。
Go 言語の型
Go 言語は静的型付け言語であり、各変数は静的型を持ち、型はコンパイル時に決定されます。
type MyInt int
var i int
var j MyInt
変数 i の型は int、変数 j の型は MyInt であり、基本型は同じですが静的型が異なるため、型変換せずに相互に代入することはできません。
インターフェイスは重要な型であり、特定のメソッドのセットを意味します。インターフェイス変数には、io.Reader や io.Writer など、インターフェイスを実装するメソッド (インターフェイス自体を除く) の具体的な値を格納できます。
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
型宣言が Reader (または Writer) メソッドを実装する場合、それは io.Reader (または io.Writer) を実装します。これは、io.Reader の変数が Read メソッドを実装する任意の型を保持できることを意味します。
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
明確にしなければならないことの 1 つは、変数 r の特定の値が何であっても、r の型は常に io.Reader であるということです。Go 言語は静的に型指定されているため、r の静的型は io.Reader です。
インターフェイス タイプには、空のインターフェイスという非常に重要な例があります。
interface{}
これは空のメソッド セットを表し、値またはメソッドがすべてゼロであるため、すべての値がそれを満たすことができます。
Go 言語のインターフェースは動的型だという人がいますが、それは間違いで、すべて静的型であり、インターフェース変数に格納される値は実行時に変化する可能性がありますが、インターフェース変数の型は変わりません。リフレクションとインターフェースは密接な関係にあるため、これらを理解する必要があります。
インターフェースについてはここまでにして、次に Go 言語のリフレクションの 3 つの法則を見てみましょう。
リフレクションの第一法則:リフレクションは「インターフェース型変数」を「リフレクション型オブジェクト」に変換できる
注: ここでの反射タイプは、reflect.Type およびreflect.Value を指します。
使用法に関しては、リフレクションはプログラムが実行時にインターフェイス変数内に格納されている (値、型) ペアをチェックできるメカニズムを提供します。
最初に、reflect パッケージの 2 つのタイプ Type と Value を理解しましょう。これら 2 つのタイプにより、インターフェイス内のデータへのアクセスが可能になります。これらは、それぞれ、reflect.TypeOf とreflect.ValueOf という 2 つの単純なメソッドに対応します。インターフェース変数のreflect.Type部分とreflect.Value部分を読み取ります。
もちろん、reflect.Valueからreflect.Typeを取得することも簡単なので、ここでは分離しましょう。
まず、reflect.TypeOf を見てみましょう。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
操作の結果は次のようになります。
type: float64
なぜインターフェイスを見なかったのかと疑問に思われるかもしれません。このコードは、インターフェイスを渡さずに、float64 型の変数 x をreflect.TypeOf に渡すだけのように見えます。実際、reflect.TypeOf の関数シグネチャには空のインターフェイスが含まれています。
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
Reflect.TypeOf(x) を呼び出すと、x は空のインターフェイス変数に格納されて渡され、reflect.TypeOf は空のインターフェイス変数を逆アセンブルして、その型情報を復元します。
関数reflect.ValueOfも基になる値を復元します。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
}
操作の結果は次のようになります。
value: 3.4
型reflect.Typeとreflect.Valueには多くのメソッドがあり、それらを確認して使用できます。ここではいくつかの例を示します。
型reflect.Valueには、型reflect.Typeのオブジェクトを返すメソッドType()があります。
Type と Value の両方に Kind と呼ばれるメソッドがあり、基になるデータの型を示す定数を返します。一般的な値は、Uint、Float64、Slice などです。
Value 型には、基になるデータを抽出するための Int や Float に似たメソッドもいくつかあります。
- intメソッドはint64の抽出に使用されます
- float64 の抽出には Float メソッドを使用します。サンプルコードは次のとおりです。
パッケージ main
インポート (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
}
操作の結果は次のようになります。
type: float64
kind is float64: true
value: 3.4
SetInt や SetFloat などのデータを変更するメソッドもあります。これらを導入する前に、以下で詳しく説明する「設定可能性」の機能を理解する必要があります。
リフレクション ライブラリには、個別にリストして説明する価値のある多くの属性が用意されているので、それらを紹介しましょう。
1 つ目は、Value の getter メソッドと setter メソッドの導入です。API を確実に簡素化するために、これら 2 つのメソッドは、特定の型グループの中で最も範囲が広い方で動作します。たとえば、符号付き整数の処理には int64 が使用されます。これは、Value 型の Int メソッドの戻り値が int64 型であり、SetInt メソッドで受け取るパラメータの型も int64 型であることを意味します。実際に使用する場合は、実際の型に変換する必要がある場合があります。
package main
import (
"fmt"
"reflect"
)
func main(){
var x uint8 = 'x'
v :=reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint()) // v.Uint returns a uint64.
}
操作の結果は次のようになります。
type: uint8
kind is uint8: true
次に、リフレクション オブジェクトの Kind メソッドは、静的型ではなく、基になる型を記述します。反射オブジェクトにユーザー定義型の値が含まれている場合は、次のようになります。
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
上記のコードでは、変数 v の静的型は int ではなく MyInt ですが、Kind メソッドは依然としてreflect.Intを返します。つまり、Kind メソッドは、Type メソッドとは異なり、MyInt と int を区別しません。
リフレクションの第 2 法則: リフレクションは「リフレクション型オブジェクト」を「インターフェース型変数」に変換できます。
物理学におけるリフレクションと同様に、Go 言語におけるリフレクションでも、それとは反対のタイプのオブジェクトを作成できます。
Reflect.Value 型の変数に従って、Interface メソッドを使用してそのインターフェイス型の値を復元できます。実際、このメソッドは型と値の情報をパックしてインターフェイス変数に入力し、戻ります。
その関数宣言は次のとおりです。
// Interface returns v’s value as an interface{}.
func (v Value) Interface() interface{}
その後、アサーションを介して基礎となる具体的な値を復元できます。
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
上記のコードは、float64 型の値を出力します。これは、リフレクション型変数 v で表される値です。
実際、この機能をさらに活用することができます。標準ライブラリの fmt.Println や fmt.Printf などの関数はすべて、空のインターフェイス変数をパラメータとして受け取り、インターフェイス変数は fmt パッケージ内でアンパックされるため、 fmt パッケージ 関数がreflect.Value型変数のデータを出力する場合、必要なのはInterfaceメソッドの結果をフォーマッタに渡すことだけです。
fmt.Println(v.Interface())
fmt.Println(v) を使用しないのはなぜでしょうか? v の型はreflect.Valueなので、必要なのはその具体的な値です。値の型は float64 なので、浮動小数点形式で出力することもできます。
fmt.Printf(“value is %7.1e\n”, v.Interface())
操作の結果は次のようになります。
3.4e+00
同様に、今回は v.Interface() の結果に対して型アサーションを行う必要はなく、空のインターフェイス値には特定の値の型情報が含まれており、Printf 関数によって型情報が復元されます。
つまり、Interface メソッドは ValueOf 関数の逆であり、戻り値の静的型がinterface{} であるという点だけが異なります。
Go のリフレクション機構では、「インターフェース型変数」を「リフレクション型オブジェクト」に変換し、さらに「リフレクション型オブジェクト」をそこに変換することができます。
リフレクションの第 3 法則: 「リフレクション タイプ オブジェクト」を変更したい場合、その値は「書き込み可能」でなければなりません
この法則は微妙でわかりにくいですが、最初の法則から始めると理解しやすいはずです。
次のコードは正しく動作しませんが、調査する価値は十分にあります。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic
このコードを実行すると、奇妙な例外がスローされます。
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
ここでの問題は、値7.1
アドレス指定できないことではなく、変数 v が「書き込み不可」であるためです。「書き込み可能性」はリフレクション型変数のプロパティですが、すべてのリフレクション型変数がこのプロパティを持っているわけではありません。
CanSet メソッドを使用して、reflect.Value 型変数の「書き込み可能性」を確認できます。上記の例では、次のように記述できます。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("vの設定可能性:", v.CanSet())
}
操作の結果は次のようになります。
vの設定可能性: false
「書き込み可能」ではない Value 型変数の場合、Set メソッドを呼び出すとエラーが報告されます。
まず第一に、「書き込み可能性」とは何かを理解する必要があります。「書き込み可能性」はアドレス可能性と多少似ていますが、より厳密です。これはリフレクション型変数の属性であり、変数に基礎となる変数を変更する機能を与えます。ストレージデータ。 「書き込み可能性」は、最終的には反射オブジェクトがプリミティブ値を格納するかどうかによって決まります。
サンプルコードは次のとおりです。
var x float64 = 3.4
v := reflect.ValueOf(x)
ここでは、x 自体ではなく、変数 x のコピーをreflect.ValueOf 関数に渡します。次のコード行が正常に実行されるかどうかを想像してください。
v.SetFloat(7.1)
このコード行が正常に実行されると、変数 v が x から作成されているように見えますが、x は更新されません。代わりに、反射オブジェクト v 内に存在する x のコピーが更新され、変数 x 自体はまったく影響を受けません。これは紛らわしく、意味がわからないため、違法です。 「Writability」はこの問題を回避するように設計されています。
これは奇妙に思えるかもしれませんが、そうではなく、同様の状況はよくあります。次のコード行を考えてみましょう。
f(x)
コードでは、変数 x のコピーを関数に渡しているため、x の値が変更されることは想定されていません。関数 f が変数 x を変更できると予想される場合、次のように x のアドレス (つまり、x へのポインタ) を関数 f に渡す必要があります。
f(&x)
リフレクションの仕組みは同じで、リフレクションを通じて変数 x を変更したい場合は、変更したい変数のポインタをリフレクション ライブラリに渡す必要があります。
まず、通常どおり変数 x を初期化し、それを指す p という名前のリフレクション オブジェクトを作成します。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注:xのアドレスを取得してください。
fmt.Println("pのタイプ:", p.Type())
fmt.Println("pのセット可能性:", p.CanSet())
}
操作の結果は次のようになります。
type of p: *float64
settability of p: false
リフレクション オブジェクト p は書き込み可能ではありませんが、p を変更する必要はありません。実際、変更したいのは *p です。 p が指すデータを取得するには、Value 型の Elem メソッドを呼び出します。 Elem メソッドはポインターを「逆参照」し、その結果を反映された Value 型オブジェクトに格納できます。 v:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
p: = reflect.ValueOf(& x) //注:xのアドレスを取ること。
v:= p.Elem()
fmt.Println(「vの設定可能性:」, v.CanSet())
}
操作の結果は次のようになります。
「vの設定可能性:」: true
変数 v は x を表すため、v.SetFloat を使用して x の値を変更できます。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意: x のアドレスを取得します。
v := p.Elem()
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
}
操作の結果は次のようになります。
7.1
7.1
Reflection は理解するのが簡単ではありません。reflect.Type と Reflect.Value は実行されるプログラムを混乱させますが、プログラミング言語が行うこととまったく同じことを行います。覚えておいてください。反射オブジェクトは、それが表すオブジェクトを変更するときは常に、それが表すオブジェクトのアドレスを取得する必要があります。
構造
通常、構造体のフィールドを変更するにはリフレクションを使用しますが、構造体へのポインタがある限り、そのフィールドを変更できます。
以下は、構造体変数 t を解析し、構造体のアドレスを使用してリフレクション変数を作成し、それを変更する例です。次に、typeOfT をその型に設定し、単純なメソッド呼び出しでフィールドを反復処理します。
構造体の型からフィールド名を抽出しましたが、各フィールド自体は通常のreflect.Valueオブジェクトであることに注意してください。
package main
import (
"fmt"
"reflect"
)
func main() {
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
}
操作の結果は次のようになります。
0: A int = 23
1: B string = skidoo
構造体のエクスポート可能なフィールドのみが「設定可能」であるため、T フィールド名は大文字で表記されます。
s には設定可能なリフレクション オブジェクトが含まれているため、構造体のフィールドを変更できます。
package main
import (
"fmt"
"reflect"
)
func main() {
type T struct {
A int
B string
}
t:= T{23、"スキドゥ"}
s:= reflect.ValueOf(&t).Elem()
s.Field(0).SetInt(77)
s.Field(1).SetString("サンセットストリップ")
fmt.Println("tは今、",t)
}
s が (&t ではなく) t から作成されるようにプログラムを変更した場合、t のフィールドは設定できないため、プログラムは SetInt および SetString の呼び出しで失敗します。
リフレクション ルールは次のように要約できます。
- Reflection は「インターフェイス型変数」を「リフレクション型オブジェクト」に変換できます。
- Reflection は「リフレクション型オブジェクト」を「インターフェース型変数」に変換できます。
- 「反射型オブジェクト」を変更する場合、その値は「書き込み可能」である必要があります。