1. 程式人生 > 實用技巧 >【go語言學習】反射reflect

【go語言學習】反射reflect

一、認識反射

  • 維基百科中的定義:
    在電腦科學中,反射是指計算機程式在執行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力。用比喻來說,反射就是程式在執行的時候能夠“觀察”並且修改自己的行為。

不同語言的反射模型不盡相同,有些語言還不支援反射。

Go 語言支援反射,它提供了一種機制在執行時更新變數和檢查它們的值、呼叫它們的方法,但是在編譯時並不知道這些變數的具體型別。

1、為什麼要用反射
  • 需要反射的 2 個常見場景:

(1)有時你需要編寫一個函式,但是並不知道傳給你的引數型別是什麼,可能是沒約定好;也可能是傳入的型別很多,這些型別並不能統一表示。這時反射就會用的上了。
(2)有時候需要根據某些條件決定呼叫哪個函式,比如根據使用者的輸入來決定。這時就需要對函式和函式的引數進行反射,在執行期間動態地執行函式。

  • 在講反射的原理以及如何用之前,還是說幾點不使用反射的理由:

(1)與反射相關的程式碼,經常是難以閱讀的。在軟體工程中,程式碼可讀性也是一個非常重要的指標。
(2)Go 語言作為一門靜態語言,編碼過程中,編譯器能提前發現一些型別錯誤,但是對於反射程式碼是無能為力的。所以包含反射相關的程式碼,很可能會執行很久,才會出錯,這時候經常是直接 panic,可能會造成嚴重的後果。
(3)反射對效能影響還是比較大的,比正常程式碼執行速度慢一到兩個數量級。所以,對於一個專案中處於執行效率關鍵位置的程式碼,儘量避免使用反射特性。

2、反射原理

interface是 Go 語言實現抽象的一個非常強大的工具。當向介面變數賦一個實體型別的時候,介面會儲存實體的型別資訊,反射就是通過介面的型別資訊實現的,反射建立在型別的基礎上。

go語言的兩個特點:

  • go語言是靜態型別語言,因此在程式編譯階段,型別已經確定。
  • interface{}空介面可以和任意型別進行互動,因此可以利用這一特點實現對任意型別的反射。

go語言的型別:

  • 變數包含(type,value)兩個部分。
  • type 包括 static type和concrete type. 簡單來說 static type是你在編碼是看見的型別(如int、string),concrete type是runtime系統看見的型別。
  • 型別斷言能否成功,取決於變數的concrete type,而不是static type。因此,一個 reader變數如果它的concrete type也實現了write方法的話,它也可以被型別斷言為writer。

go語言的反射是建立在型別的基礎上的,golang中對於指定型別的變數的型別是靜態的(如指定為int、string型別的變數 ,他們的type是static type),也就是說在建立變數的時候型別已經確定。反射主要針對interface型別(它的type是concrete type)。

在go語言的實現中,每一個interface變數都有一個pair,pair中記錄了實際變數的值和型別:

(value, type)

其中:value是實際變數值,type是實際變數型別。一個interface變數包含了2個指標,一個指向值的型別(對應concrete type),一個指向實際的值(對應value)。

程式碼示例:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// 建立一個*os.File型別的變數f
	f, _ := os.OpenFile("a.txt", os.O_RDWR, os.ModePerm)
	// 建立一個io.Reader介面型別的變數r
	var r io.Reader
	r = f.(io.Reader)
	// 建立一個io.Writer介面型別的變數w
	var w io.Writer
	w = r.(io.Writer)
	// 建立一個interface{}空介面型別的變數i
	var i interface{}
	i = w
	fmt.Printf("%T, %T, %T, %T", f, r, w, i)
}

執行結果

*os.File, *os.File, *os.File, *os.File

可以看到,介面變數w、r、i的pair相同,都是:(f, *os.File)。

interface及其pair的存在,是Golang中實現反射的前提,理解了pair,就更容易理解反射。反射就是用來檢測儲存在介面變數內部(值value;型別concrete type) pair對的一種機制。

下面,我們通過go語言的reflect包提供的API來實現反射機制

二、Type和Value

reflect包提供了兩種型別(或者說兩個方法)讓我們可以很容易的訪問介面變數內容,分別是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解釋:

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
func ValueOf(i interface{}) Value {...}

翻譯一下:ValueOf用來獲取輸入引數介面中的資料的值,如果介面為空則返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

翻譯一下:TypeOf用來動態獲取輸入引數介面中的值的型別,如果介面為空則返回nil

reflect.TypeOf()是獲取pair中的type,reflect.ValueOf()獲取pair中的value。

首先需要把它轉化成reflect物件(reflect.Type或者reflect.Value,根據不同的情況呼叫不同的函式。

說明:

  1. reflect.TypeOf: 直接給到了我們想要的type型別,如float64int、各種pointerstruct 等等真實的型別
  2. reflect.ValueOf:直接給到了我們想要的具體的值,如1.2345這個具體數值,或者類似&{1, "Allen.Wu", 25}這樣的結構體struct的值
  3. 也就是說明反射可以將“介面型別變數”轉換為“反射型別物件”,反射型別指的是reflect.Type和reflect.Value這兩種

Type 和 Value 都包含了大量的方法,其中第一個有用的方法應該是 Kind,這個方法返回該型別的具體資訊:Uint、Float64 等。Value 型別還包含了一系列型別方法,比如 Int(),用於返回對應的值。以下是Kind的種類:

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

三、反射的規則

下圖描述了例項、Value、Type 三者之間的轉換關係:

1、從例項到Value

func ValueOf(i interface {}) Value

2、從例項到Type

func TypeOf(i interface{}) Type

3、從Type到Value

Type 裡面只有型別資訊,所以直接從一個 Type 介面變數裡面是無法獲得例項的 Value 的,但可以通過該 Type 構建一個新例項的 Value。reflect 包提供了兩種方法,示例如下:

//New 返回的是一個 Value,該 Value 的 type 為 PtrTo(typ),即 Value 的 Type 是指定 typ 的指標型別
func New(typ Type) Value
//Zero 返回的是一個 typ 型別的零佳,注意返回的 Value 不能定址,位不可改變
func Zero(typ Type) Value

4、從Value到Type

從反射物件 Value 到 Type 可以直接呼叫 Value 的方法,因為 Value 內部存放著到 Type 型別的指標。

func (v Value) Type() Type

5、從Value到例項

Value 本身就包含型別和值資訊,reflect 提供了豐富的方法來實現從 Value 到例項的轉換。例如:

//該方法最通用,用來將 Value 轉換為空介面,該空介面內部存放具體型別例項
//可以使用介面型別查詢去還原為具體的型別
func (v Value) Interface() (i interface{})

//Value 自身也提供豐富的方法,直接將 Value 轉換為簡單型別例項,如果型別不匹配,則直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64

6、從 Value 的指標到值

從一個指標型別的 Value 獲得值型別 Value 有兩種方法,示例如下。

//如果 v 型別是介面,則 Elem() 返回介面繫結的例項的 Value,如採 v 型別是指標,則返回指標值的 Value,否則引起 panic
func (v Value) Elem() Value
//如果 v 是指標,則返回指標值的 Value,否則返回 v 自身,該函式不會引起 panic
func Indirect(v Value) Value

7、Type 指標和值的相互轉換

指標型別 Type 到值型別 Type。例如:

//t 必須是 Array、Chan、Map、Ptr、Slice,否則會引起 panic
//Elem 返回的是其內部元素的 Type
t.Elem() Type

值型別 Type 到指標型別 Type。例如:

//PtrTo 返回的是指向 t 的指標型 Type
func PtrTo(t Type) Type

8、Value 值的可修改性

Value 值的修改涉及如下兩個方法:

//通過 CanSet 判斷是否能修改
func (v Value ) CanSet() bool
//通過 Set 進行修改
func (v Value ) Set(x Value)

根據 Go 官方關於反射的部落格,反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

第一條是最基本的:反射可以從介面值得到反射物件。

第二條實際上和第一條是相反的機制,反射可以從反射物件獲得介面值。

第三條不太好懂:如果需要操作一個反射變數,則其值必須可以修改。

四、反射的使用

1、從relfect.Value中獲取介面interface的資訊

當執行reflect.ValueOf(interface)之後,就得到了一個型別為”relfect.Value”變數,可以通過它本身的Interface()方法獲得介面變數的真實內容,然後可以通過型別判斷進行轉換,轉換為原有真實型別。

已知型別

從反射值物件(reflect.Value)中獲取值得方法:

方法名 說 明
Interface() interface{} 將值以 interface{} 型別返回,可以通過型別斷言轉換為指定型別
Int() int64 將值以 int 型別返回,所有有符號整型均可以此方式返回
Uint() uint64 將值以 uint 型別返回,所有無符號整型均可以此方式返回
Float() float64 將值以雙精度(float64)型別返回,所有浮點數(float32、float64)均可以此方式返回
Bool() bool 將值以 bool 型別返回
Bytes() []bytes 將值以位元組陣列 []bytes 型別返回
String() string 將值以字串型別返回

已知型別後轉換為其對應的型別的做法如下,直接通過Interface方法然後強制轉換,如下:

realValue := value.Interface().(已知的型別)

示例程式碼:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a int = 10
	rType := reflect.TypeOf(a)
	fmt.Println(rType)
	rValue := reflect.ValueOf(a)
	fmt.Println(rValue)
	rPointer := reflect.ValueOf(&a)
	fmt.Println(rPointer)
	// 1. 轉換的時候,如果轉換的型別不完全符合,則直接panic,型別要求非常嚴格!
	// 2. 轉換的時候,要區分是指標還是值
	// 3. 也就是說反射可以將“反射型別物件”再重新轉換為“介面型別變數”
	// convertValue := rValue.Interface().(int) // 10
	convertValue := rValue.Int()
	// convertValue := rValue.Bool() // panic: reflect: call of reflect.Value.Bool on int Value
	convertPointer := rPointer.Interface().(*int)
	fmt.Println(convertValue)
	fmt.Println(convertPointer)
}

執行結果

int
10
0xc000012090
10
0xc000012090

結構體型別

反射值物件(reflect.Value)提供對結構體訪問的方法,通過這些方法可以完成對結構體欄位和方法的訪問,如下表所示。

方 法 說 明
Field(i int) Value 根據索引,返回索引對應的結構體成員欄位的反射值物件。當值不是結構體或索引超界時發生panic
NumField() int 返回結構體成員欄位數量。當值不是結構體或索引超界時發生panic
FieldByName(name string) Value 根據給定字串返回字串對應的結構體欄位。沒有找到時返回零值,當值不是結構體或索引超界時發生panic
FieldByIndex(index []int) Value 多層成員訪問時,根據 []int 提供的每個結構體的欄位索引,返回欄位的值。 沒有找到時返回零值,當值不是結構體或索引超界時發生panic
FieldByNameFunc(match func(string) bool) Value 根據匹配函式匹配需要的欄位。找到時返回零值,當值不是結構體或索引超界時發生panic
NumMethod() int 返回該型別的方法集中方法的數目
Method(int) Method 返回該型別方法集中的第i個方法
MethodByName(string)(Method, bool) 根據方法名返回該型別方法集中的方法

示例程式碼:

package main

import (
	"fmt"
	"reflect"
)

// Student 學生結構體
type Student struct {
	Name    string
	Age     int
	Address string
}

// Say Student結構體的方法
func (s Student) Say(msg string) {
	fmt.Println(msg)
}

// PrintInfo Student結構體的方法
func (s Student) PrintInfo() {
	fmt.Printf("姓名:%s\t年齡:%d\t地址:%s\n", s.Name, s.Age, s.Address)
}

func main() {
	s := Student{"tom", 20, "上海市"}
	Test(s)
}

// Test 測試函式
func Test(i interface{}) {
	// 獲取i的型別
	rType := reflect.TypeOf(i)
	fmt.Println("i的型別是:", rType.Name()) // Student
	fmt.Println("i的種類是:", rType.Kind()) // struct
	// 獲取i的值
	rValue := reflect.ValueOf(i)
	fmt.Println("i的值是:", rValue)
	// 獲取i的欄位資訊
	for i := 0; i < rValue.NumField(); i++ {
		field := rType.Field(i)
		value := rValue.Field(i).Interface()
		fmt.Printf("欄位名稱:%s, 欄位型別:%s, 欄位值:%v\n", field.Name, field.Type, value)
	}
	// 獲取i的方法資訊
	for i := 0; i < rValue.NumMethod(); i++ {
		method := rType.Method(i)
		fmt.Printf("方法的名稱:%s, 方法的型別:%s\n", method.Name, method.Type)

	}
}

執行結果:

i的型別是: Student
i的種類是: struct
i的值是: {tom 20 上海市}
欄位名稱:Name, 欄位型別:string, 欄位值:tom
欄位名稱:Age, 欄位型別:int, 欄位值:20
欄位名稱:Address, 欄位型別:string, 欄位值:上海市
方法的名稱:PrintInfo, 方法的型別:func(main.Student)
方法的名稱:Say, 方法的型別:func(main.Student, string)
2、通過reflect.Value設定實際變數的值

reflect.Value是通過reflect.ValueOf(X)獲得的,只有當X是指標的時候,才可以通過reflec.Value修改實際變數X的值,即:要修改反射型別的物件就一定要保證其值是“addressable”的。

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value {}
// 翻譯:
// Elem返回介面v包含的值或指標v指向的值。
// 如果v的型別不是interface或ptr,它會恐慌。
// 如果v為零,則返回零值。

示例程式碼:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a int = 10
	rValue := reflect.ValueOf(&a)
	fmt.Println("是否可修改:", rValue.Elem().CanSet())
	rValue.Elem().SetInt(20)
	fmt.Println(a)
}

執行結果

是否可修改: true
20
3、通過reflect.Value來進行函式的呼叫

如果反射值物件(reflect.Value)中值的型別為函式時,可以通過 reflect.Value 呼叫該函式。使用反射呼叫函式時,需要將引數使用反射值物件的切片 []reflect.Value 構造後傳入 Call() 方法中,呼叫完成時,函式的返回值通過 []reflect.Value 返回。

示例程式碼:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	rValue := reflect.ValueOf(add)
	res := rValue.Call([]reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)})
	fmt.Printf("%v, %T\n", res, res)
	fmt.Println(res[0].Int())
}

func add(a, b int) int {
	return a + b
}

執行結果

[<int Value>], []reflect.Value
30

提示:

反射呼叫函式的過程需要構造大量的 reflect.Value 和中間變數,對函式引數值進行逐一檢查,還需要將呼叫引數複製到呼叫函式的引數記憶體中。呼叫完畢後,還需要將返回值轉換為 reflect.Value,使用者還需要從中取出呼叫值。因此,反射呼叫函式的效能問題尤為突出,不建議大量使用反射函式呼叫。

4、通過反射,呼叫方法

呼叫方法和呼叫函式是一樣的,只不過結構體需要先通過rValue.Method()先獲取方法再呼叫。

示例程式碼:

package main

import (
	"fmt"
	"reflect"
)

// Student 學生結構體
type Student struct {
	Name    string
	Age     int
	Address string
}

// Say Student結構體的方法
func (s Student) Say(msg string) {
	fmt.Println(msg)
}

// PrintInfo Student結構體的方法
func (s Student) PrintInfo() {
	fmt.Printf("姓名:%s\t年齡:%d\t地址:%s\n", s.Name, s.Age, s.Address)
}

func main() {
	s := Student{"tom", 20, "上海市"}

	rValue := reflect.ValueOf(s)
	m1 := rValue.MethodByName("Say")
	m1.Call([]reflect.Value{reflect.ValueOf("hello world")})
	m2 := rValue.MethodByName("PrintInfo")
	m2.Call(nil)
}

執行結果

hello world
姓名:tom       年齡:20        地址:上海市