Golang通脈之反射

發表於2021-10-27

什麼是反射

官方關於反射定義:

Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion.
(在計算機領域,反射是一種讓程式——主要是通過型別——理解其自身結構的一種能力。它是超程式設計的組成之一,同時它也是一大引人困惑的難題。)

維基百科關於反射的定義:

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

《Go語言聖經》關於反射的定義:

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

Go 語言是靜態編譯類語言,比如在定義一個變數的時候,已經知道了它是什麼型別。但是有些事情只有在執行時才知道。比如定義了一個函式,它有一個interface{}型別的引數,這也就意味著呼叫者可以傳遞任何型別的引數給這個函式。在這種情況下,如果想知道呼叫者傳遞的是什麼型別的引數,就需要用到反射。如果想知道一個結構體有哪些欄位和方法,也需要反射。

根據以上定義,可以得出:

反射是指在程式執行時對程式本身進行訪問和修改的能力。程式在編譯時,變數被轉換為記憶體地址,變數名不會被編譯器寫入到可執行部分。在執行程式時,程式無法獲取自身的資訊。

支援反射的語言可以在程式編譯期將變數的反射資訊,如欄位名稱、型別資訊、結構體資訊等整合到可執行檔案中,並給程式提供介面訪問反射資訊,這樣就可以在程式執行期獲取型別的反射資訊,並且有能力修改它們。

變數的內在機制

  1. Go語言中的變數是分為兩部分的:

    • 型別資訊(type):預先定義好的元資訊。
    • 值資訊(value):程式執行過程中可動態變化的。

    理解這一點就知道為什麼nil != nil

  2. type 包括 static typeconcrete type. 簡單來說 static type是在編碼時確定的型別(如intstring等),concrete typeruntime系統確定的型別。

  3. 型別斷言能否成功,取決於變數的concrete type,而不是static type。因此,一個 reader變數如果它的concrete type也實現了write方法的話,它也可以被型別斷言為writer

Go是靜態型別語言。每個變數都擁有一個靜態型別,這意味著每個變數的型別在編譯時都是確定的:int,float32, *AutoType, []byte, chan []int 諸如此類。

在反射的概念中, 編譯時就知道變數型別的是靜態型別;執行時才知道一個變數型別的叫做動態型別

  • 靜態型別: 靜態型別就是變數宣告時的賦予的型別
type MyInt int // int 就是靜態型別

type A struct{
   Name string  // string就是靜態
}
var i *int  // *int就是靜態型別
  • 動態型別:執行時給這個變數賦值時,這個值的型別(如果值為nil的時候沒有動態型別)。一個變數的動態型別在執行時可能改變,這主要依賴於它的賦值(前提是這個變數是介面型別)。
var A interface{} // 靜態型別interface{}
A = 10            // 靜態型別為interface{}  動態為int
A = "String"      // 靜態型別為interface{}  動態為string
var M *int
A = M             // A的值可以改變

Go語言的反射就是建立在型別之上的,Golang的指定型別的變數的型別是靜態的,在建立變數的時候就已經確定,反射主要與Golang的interface型別相關,只有interface型別才有反射一說

在Golang的實現中,每個interface變數都有一個對應pairpair中記錄了實際變數的值和型別(在介面介紹時有描述):

(value, type)

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

例如,建立型別為*os.File的變數,然後將其賦給一個介面變數r

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty

介面變數rpair中將記錄如下資訊:(tty, *os.File),這個pair在介面變數的連續賦值過程中是不變的,將介面變數r賦給另一個介面變數w:

var w io.Writer
w = r.(io.Writer)

介面變數wpairrpair相同,都是:(tty, *os.File),即使w是空介面型別,pair也是不變的。

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

所以要理解兩個基本概念 Type 和 Value,它們也是 Go語言包中 reflect 空間裡最重要的兩個型別。

reflect

Go程式在執行時使用reflect包訪問程式的反射資訊。

之前介紹過interface,空介面可以儲存任意型別的變數,那如何知道這個空介面儲存的資料是什麼呢? 反射就是在執行時動態的獲取一個變數的型別資訊和值資訊。

在Go語言的反射機制中,任何介面值都由是一個具體型別具體型別的值兩部分組成的。 在Go語言中反射的相關功能由內建的reflect包提供,任意介面值在反射中都可以理解為由reflect.Typereflect.Value兩部分組成,並且reflect包提供了reflect.TypeOfreflect.ValueOf兩個函式來獲取任意物件的ValueType

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
// 翻譯一下:ValueOf用來獲取輸入引數介面中的資料的值,如果介面為空則返回0
func ValueOf(i interface{}) Value {...}

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
// 翻譯一下:TypeOf用來動態獲取輸入引數介面中的值的型別,如果介面為空則返回nil
func TypeOf(i interface{}) Type {...}

TypeOf

Type

reflect.Value 可以用於與值有關的操作中,而如果是和變數型別本身有關的操作,則最好使用 reflect.Type,比如要獲取結構體對應的欄位名稱或方法。

和 reflect.Value 不同,reflect.Type 是一個介面,而不是一個結構體,所以也只能使用它的方法。

以下 reflect.Type 介面常用的方法。從列表來看,大部分都和 reflect.Value 的方法功能相同。

type Type interface {

   Implements(u Type) bool
   AssignableTo(u Type) bool
   ConvertibleTo(u Type) bool
   Comparable() bool

   //以下這些方法和Value結構體的功能相同
   Kind() Kind

   Method(int) Method
   MethodByName(string) (Method, bool)
   NumMethod() int
   Elem() Type
   Field(i int) StructField
   FieldByIndex(index []int) StructField
   FieldByName(name string) (StructField, bool)
   FieldByNameFunc(match func(string) bool) (StructField, bool)
   NumField() int
}

其中幾個特有的方法如下:

  1. Implements 方法用於判斷是否實現了介面 u;
  2. AssignableTo 方法用於判斷是否可以賦值給型別 u,其實就是是否可以使用 =,即賦值運算子;
  3. ConvertibleTo 方法用於判斷是否可以轉換成型別 u,其實就是是否可以進行型別轉換;
  4. Comparable 方法用於判斷該型別是否是可比較的,其實就是是否可以使用關係運算子進行比較。

要反射獲取一個變數的 reflect.Type,可以通過函式 reflect.TypeOf(),程式通過型別物件可以訪問任意值的型別資訊。

func main() {
	//反射操作:通過反射,可以獲取一個介面型別變數的 型別
	var x float64 =3.4
	fmt.Println("type:",reflect.TypeOf(x)) //type: float64
}

type nametype kind

在反射中關於型別還劃分為兩種:型別(Type)種類(Kind)。因為在Go語言中可以使用type關鍵字構造很多自定義型別,而種類(Kind)就是指底層的型別,但在反射中,當需要區分指標、結構體等大品種的型別時,就會用到種類(Kind)。 舉個例子,定義了兩個指標型別和兩個結構體型別,通過反射檢視它們的型別和種類。

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:  25,
	}
	var e = book{title: "《Go語言聖經》"}
	reflectType(d) // type:person kind:struct
	reflectType(e) // type:book kind:struct
}

Go語言的反射中像陣列、切片、Map、指標等型別的變數,它們的.Name()都是返回

reflect包中定義的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                 // 有符號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        // 底層指標
)

通過 reflect.Type 還可以判斷是否實現了某介面。以 person 結構體為例,判斷它是否實現了介面 fmt.Stringerio.Writer

func main() {
   p:=person{Name: "張三",Age: 20}
   pt:=reflect.TypeOf(p)
   stringerType:=reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
   writerType:=reflect.TypeOf((*io.Writer)(nil)).Elem()
   fmt.Println("是否實現了fmt.Stringer:",pt.Implements(stringerType))
   fmt.Println("是否實現了io.Writer:",pt.Implements(writerType))
}

儘可能通過型別斷言的方式判斷是否實現了某介面,而不是通過反射。

通過 Implements 方法來判斷是否實現了 fmt.Stringer 和 io.Writer 介面,執行結果:

是否實現了fmt.Stringer: false
是否實現了io.Writer: false

ValueOf

reflect.ValueOf()返回的是reflect.Value型別,其中包含了原始值的值資訊。reflect.Value與原始值之間可以互相轉換。

reflect.Value 被定義為一個 struct 結構體,它的定義如下面所示:

// Value is the reflection interface to a Go value.
//
// Not all methods apply to all kinds of values. Restrictions,
// if any, are noted in the documentation for each method.
// Use the Kind method to find out the kind of value before
// calling kind-specific methods. Calling a method
// inappropriate to the kind of type causes a run time panic.
//
// The zero Value represents no value.
// Its IsValid method returns false, its Kind method returns Invalid,
// its String method returns "<invalid Value>", and all other methods panic.
// Most functions and methods never return an invalid value.
// If one does, its documentation states the conditions explicitly.
//
// A Value can be used concurrently by multiple goroutines provided that
// the underlying Go value can be used concurrently for the equivalent
// direct operations.
//
// To compare two Values, compare the results of the Interface method.
// Using == on two Values does not compare the underlying values
// they represent.
type Value struct {
	// typ holds the type of the value represented by a Value.
	typ *rtype

	// Pointer-valued data or, if flagIndir is set, pointer to data.
	// Valid when either flagIndir is set or typ.pointers() is true.
	ptr unsafe.Pointer

	// flag holds metadata about the value.
	// The lowest bits are flag bits:
	//	- flagStickyRO: obtained via unexported not embedded field, so read-only
	//	- flagEmbedRO: obtained via unexported embedded field, so read-only
	//	- flagIndir: val holds a pointer to the data
	//	- flagAddr: v.CanAddr is true (implies flagIndir)
	//	- flagMethod: v is a method value.
	// The next five bits give the Kind of the value.
	// This repeats typ.Kind() except for method values.
	// The remaining 23+ bits give a method number for method values.
	// If flag.kind() != Func, code can assume that flagMethod is unset.
	// If ifaceIndir(typ), code can assume that flagIndir is set.
	flag

	// A method value represents a curried method invocation
	// like r.Read for some receiver r. The typ+val+flag bits describe
	// the receiver r, but the flag's Kind bits say Func (methods are
	// functions), and the top bits of the flag give the method number
	// in r's type's method table.
}

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 將值以字串型別返回
CanSet() bool 是否可以修改對應的值
Elem() Type 獲取指標指向的值,一般用於修改對應的值
Kind() Kind 獲取對應的型別類別,比如Array、Slice、Map等

通過反射獲取值

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()方法來獲取指標對應的值。

func main() {
	var a int64 = 10
	v := reflect.ValueOf(a)
	if v.Kind() == reflect.Int64 {
		v.SetInt(20) //panic: reflect: reflect.Value.SetInt using unaddressable value
	}
}
func main() {
	var a int64 = 10
	v := reflect.ValueOf(&a)    //反射獲取指標的地址
	// 反射中使用 Elem()方法獲取指標對應的值
	if v.Elem().Kind() == reflect.Int64 {
		v.Elem().SetInt(20)
	}
	fmt.Println(a) 	//20
}

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()) //var a *int IsNil: true
	// nil值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) //nil IsValid: false
	// 例項化一個匿名結構體
	b := struct{}{}
	// 嘗試從結構體中查詢"abc"欄位
	fmt.Println("b結構體是否存在成員abc:", reflect.ValueOf(b).FieldByName("abc").IsValid()) //b結構體是否存在成員abc: false
	// 嘗試從結構體中查詢"abc"方法
	fmt.Println("b結構體是否存在方法abc:", reflect.ValueOf(b).MethodByName("abc").IsValid()) //b結構體是否存在方法abc: false
	// map
	c := map[string]int{}
	// 嘗試從map中查詢一個不存在的鍵
	fmt.Println("map中是否存在鍵張三:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("張三")).IsValid()) //map中是否存在鍵張三: false
}

結構體反射

與結構體相關的方法

任意值通過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 {
    // 參見http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name    string		// Name是欄位的名字。
    PkgPath string		// PkgPath是非匯出欄位的包路徑,對匯出欄位該欄位為""。
    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() {
	stu := student{
		Name:  "張三",
		Score: 90,
	}

	t := reflect.TypeOf(stu)
	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"))
	}
}

執行結果:

student struct
name:Name index:[0] type:string json tag:name
name:Score index:[1] type:int json tag:score
name:Score index:[1] type:int json tag:score

接下來編寫一個函式printMethod(s interface{})來遍歷列印s包含的方法。

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 型別
		args := []reflect.Value{}
		v.Method(i).Call(args)
	}
}

執行結果:

2
method name:Sleep
method:func() string
睡覺
method name:Study
method:func() string
學習

反射的規則

根據上面對反射的大致介紹,對反射有了一定的瞭解,其實反射的操作步驟非常的簡單,就是通過例項物件獲取反射物件(Value、Type),然後操作相應的方法

例項、Value、Type 三者之間的轉換關係:

  1. 從例項到Value

通過例項獲取 Value 物件,直接使用 reflect.ValueOf() 函式:

func ValueOf(i interface {}) Value
  1. 從例項到Type

通過例項獲取反射物件的 Type,直接使用 reflect.TypeOf() 函式:

func TypeOf(i interface{}) Type
  1. TypeValue

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

如果知道一個型別值的底層存放地址,則還有一個函式是可以依據 type 和該地址值恢復出 Value 的:

func NewAt(typ Type, p unsafe.Pointer) Value
  1. ValueType

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

func (v Value) Type() Type
  1. 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
......
  1. Value的指標到值

從一個指標型別的 Value 獲得值型別 Value 有兩種方法:

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

指標型別 Type 到值型別 Type:

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

值型別 Type 到指標型別 Type:

//PtrTo 返回的是指向 t 的指標型 Type
func PtrTo(t Type) Type
  1. Value 值的可修改性

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

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

例項物件傳遞給介面的是一個完全的值拷貝,如果呼叫反射的方法 reflect.ValueOf() 傳進去的是一個值型別變數, 則獲得的 Value 實際上是原物件的一個副本,這個 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. 任何介面值 interface{} 都可以反射出反射物件,也就是 reflect.Value 和 reflect.Type,通過函式 reflect.ValueOf 和 reflect.TypeOf 獲得。
  2. 反射物件也可以還原為 interface{} 變數,也就是第 1 條定律的可逆性,通過 reflect.Value 結構體的 Interface 方法獲得。
  3. 要修改反射的物件,該值必須可設定,也就是可定址。

任何型別的變數都可以轉換為空介面 intferface{},所以第 1 條定律中函式 reflect.ValueOf 和 reflect.TypeOf 的引數就是 interface{},表示可以把任何型別的變數轉換為反射物件。在第 2 條定律中,reflect.Value 結構體的 Interface 方法返回的值也是 interface{},表示可以把反射物件還原為對應的型別變數。

反射的使用

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

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

已知原有型別

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

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

func main() {
	var num float64 = 1.2345

	pointer := reflect.ValueOf(&num)
	value := reflect.ValueOf(num)

	// 可以理解為“強制轉換”,但是需要注意的時候,轉換的時候,如果轉換的型別不完全符合,則直接panic
	// Golang 對型別要求非常嚴格,型別一定要完全符合
	// 如下兩個,一個是*float64,一個是float64,如果弄混,則會panic
	convertPointer := pointer.Interface().(*float64)
	convertValue := value.Interface().(float64)

	fmt.Println(convertPointer)
	fmt.Println(convertValue)
}

執行結果:

0xc000098000
1.2345

說明

  1. 轉換的時候,如果轉換的型別不完全符合,則直接panic,型別要求非常嚴格!
  2. 轉換的時候,要區分是指標還是值
  3. 也就是說反射可以將“反射型別物件”再重新轉換為“介面型別變數”

未知原有型別

很多情況下,可能並不知道其具體型別,那麼就需要進行遍歷探測其Filed來得知:

type Person struct {
	Name string
	Age int
	Sex string
}

func (p Person) Say(msg string)  {
	fmt.Println("hello,",msg)
}
func (p Person) PrintInfo()  {
	fmt.Printf("姓名:%s,年齡:%d,性別:%s\n",p.Name,p.Age,p.Sex)
}



func main() {
	p1 := Person{"張三",25,"男"}
	DoFiledAndMethod(p1)
}

// 通過介面來獲取任意引數
func DoFiledAndMethod(input interface{}) {

	getType := reflect.TypeOf(input) //先獲取input的型別
	fmt.Println("get Type is :", getType.Name()) // Person
	fmt.Println("get Kind is : ", getType.Kind()) // struct

	getValue := reflect.ValueOf(input)
	fmt.Println("get all Fields is:", getValue) //{張三 25 男}

	// 獲取方法欄位
	// 1. 先獲取interface的reflect.Type,然後通過NumField進行遍歷
	// 2. 再通過reflect.Type的Field獲取其Field
	// 3. 最後通過Field的Interface()得到對應的value
	for i := 0; i < getType.NumField(); i++ {
		field := getType.Field(i)
		value := getValue.Field(i).Interface() //獲取第i個值
		fmt.Printf("欄位名稱:%s, 欄位型別:%s, 欄位數值:%v \n", field.Name, field.Type, value)
	}

	// 通過反射,操作方法
	// 1. 先獲取interface的reflect.Type,然後通過.NumMethod進行遍歷
	// 2. 再公國reflect.Type的Method獲取其Method
	for i := 0; i < getType.NumMethod(); i++ {
		method := getType.Method(i)
		fmt.Printf("方法名稱:%s, 方法型別:%v \n", method.Name, method.Type)
	}
}

執行結果:

get Type is : Person
get Kind is :  struct
get all Fields is: {張三 25 男}
欄位名稱:Name, 欄位型別:string, 欄位數值:張三 
欄位名稱:Age, 欄位型別:int, 欄位數值:25 
欄位名稱:Sex, 欄位型別:string, 欄位數值:男 
方法名稱:PrintInfo, 方法型別:func(main.Person) 
方法名稱:Say, 方法型別:func(main.Person, string) 

說明

通過執行結果可以得知獲取未知型別的interface的具體變數及其型別的步驟為:

  1. 先獲取interface的reflect.Type,然後通過NumField進行遍歷
  2. 再通過reflect.Type的Field獲取其Field
  3. 最後通過Field的Interface()得到對應的value

通過執行結果可以得知獲取未知型別的interface的所屬方法(函式)的步驟為:

  1. 先獲取interface的reflect.Type,然後通過NumMethod進行遍歷
  2. 再分別通過reflect.Type的Method獲取對應的真實的方法(函式)
  3. 最後對結果取其Name和Type得知具體的方法名
  4. 也就是說反射可以將“反射型別物件”再重新轉換為“介面型別變數”
  5. struct 或者 struct 的巢狀都是一樣的判斷處理方式

如果是struct的話,可以使用Elem()

tag := t.Elem().Field(0).Tag //獲取定義在struct裡面的Tag屬性
name := v.Elem().Field(0).String() //獲取儲存在第一個欄位裡面的值

通過reflect.Value設定實際變數的值

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

這裡需要一個方法:

// 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為零,則返回零值。

func main() {

	var num float64 = 1.2345
	fmt.Println("old value of pointer:", num)

	// 通過reflect.ValueOf獲取num中的reflect.Value,注意,引數必須是指標才能修改其值
	pointer := reflect.ValueOf(&num)
	newValue := pointer.Elem()

	fmt.Println("type of pointer:", newValue.Type())
	fmt.Println("settability of pointer:", newValue.CanSet())

	// 重新賦值
	newValue.SetFloat(77)
	fmt.Println("new value of pointer:", num)

	////////////////////
	// 如果reflect.ValueOf的引數不是指標,會如何?
	//pointer = reflect.ValueOf(num)
	//newValue = pointer.Elem() // 如果非指標,這裡直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

執行結果:

old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77

說明

  1. 需要傳入的引數是* float64這個指標,然後可以通過pointer.Elem()去獲取所指向的Value,注意一定要是指標
  2. 如果傳入的引數不是指標,而是變數,那麼
    • 通過Elem獲取原始值對應的物件則直接panic
    • 通過CanSet方法查詢是否可以設定返回false
  3. newValue.CantSet()表示是否可以重新設定其值,如果輸出的是true則可修改,否則不能修改,修改完之後再進行列印發現真的已經修改了。
  4. reflect.Value.Elem() 表示獲取原始值對應的反射物件,只有原始物件才能修改,當前反射物件是不能修改的
  5. 也就是說如果要修改反射型別物件,其值必須是可定址的【對應的要傳入的是指標,同時要通過Elem方法獲取原始值對應的反射物件】
  6. struct 或者 struct 的巢狀都是一樣的判斷處理方式

通過reflect.Value來進行方法的呼叫

在專案應用中,另外一個常用並且屬於高階的用法,就是通過reflect來進行方法的呼叫。比如要做框架工程的時候,需要可以隨意擴充套件方法,或者說使用者可以自定義方法,關鍵點在於使用者的自定義方法是未可知的,因此可以通過reflect來搞定。

Call()方法:

// Call calls the function v with the input arguments in.
// For example, if len(in) == 3, v.Call(in) represents the Go call v(in[0], in[1], in[2]).
// Call panics if v's Kind is not Func.
// It returns the output results as Values.
// As in Go, each input argument must be assignable to the
// type of the function's corresponding input parameter.
// If v is a variadic function, Call creates the variadic slice parameter
// itself, copying in the corresponding values.
func (v Value) Call(in []Value) []Value

通過反射,呼叫方法。

type Person struct {
	Name string
	Age int
	Sex string
}

func (p Person) Say(msg string)  {
	fmt.Println("hello,",msg)
}
func (p Person) PrintInfo()  {
	fmt.Printf("姓名:%s,年齡:%d,性別:%s\n",p.Name,p.Age,p.Sex)
}

func (p Person) Test(i,j int,s string){
	fmt.Println(i,j,s)
}


// 如何通過反射來進行方法的呼叫?
// 本來可以用結構體物件.方法名稱()直接呼叫的,
// 但是如果要通過反射,
// 那麼首先要將方法註冊,也就是MethodByName,然後通過反射調動mv.Call

func main() {
	p2 := Person{"張三",25,"男"}
	// 1. 要通過反射來呼叫起對應的方法,必須要先通過reflect.ValueOf(interface)來獲取到reflect.Value,
	// 得到“反射型別物件”後才能做下一步處理
	getValue := reflect.ValueOf(p2)

	// 2.一定要指定引數為正確的方法名
	// 先看看沒有引數的呼叫方法

	methodValue1 := getValue.MethodByName("PrintInfo")
	fmt.Printf("Kind : %s, Type : %s\n",methodValue1.Kind(),methodValue1.Type())
	methodValue1.Call(nil) //沒有引數,直接寫nil

	args1 := make([]reflect.Value, 0) //或者建立一個空的切片也可以
	methodValue1.Call(args1)

	// 有引數的方法呼叫
	methodValue2 := getValue.MethodByName("Say")
	fmt.Printf("Kind : %s, Type : %s\n",methodValue2.Kind(),methodValue2.Type())
	args2 := []reflect.Value{reflect.ValueOf("反射機制")}
	methodValue2.Call(args2)

	methodValue3 := getValue.MethodByName("Test")
	fmt.Printf("Kind : %s, Type : %s\n",methodValue3.Kind(),methodValue3.Type())
	args3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200),reflect.ValueOf("Hello")}

	methodValue3.Call(args3)
}

執行結果:

Kind : func, Type : func()
姓名:張三,年齡:25,性別:男
姓名:張三,年齡:25,性別:男
Kind : func, Type : func(string)
hello, 反射機制
Kind : func, Type : func(int, int, string)
100 200 Hello

通過反射,呼叫函式。

函式像普通的變數一樣,是可以把函式作為一種變數型別的,而且是引用型別。如果說Fun()是一個函式,那麼f1 := Fun也是可以的,那麼f1也是一個函式,如果直接呼叫f1(),那麼執行的就是Fun()函式。

那麼就先通過ValueOf()來獲取函式的反射物件,可以判斷它的Kind,是一個func,那麼就可以執行Call()進行函式的呼叫。

func main() {
	//函式的反射
	f1 := fun1
	value := reflect.ValueOf(f1)
	fmt.Printf("Kind : %s , Type : %s\n",value.Kind(),value.Type()) //Kind : func , Type : func()

	value2 := reflect.ValueOf(fun2)
	fmt.Printf("Kind : %s , Type : %s\n",value2.Kind(),value2.Type()) //Kind : func , Type : func(int, string)


	//通過反射呼叫函式
	value.Call(nil)

	value2.Call([]reflect.Value{reflect.ValueOf(100),reflect.ValueOf("hello")})

}

func fun1(){
	fmt.Println("函式fun1(),無參。。")
}

func fun2(i int, s string){
	fmt.Println("函式fun2(),有引數。。",i,s)
}

說明

  1. 要通過反射來呼叫起對應的方法,必須要先通過reflect.ValueOf(interface)來獲取到reflect.Value,得到“反射型別物件”後才能做下一步處理
  2. reflect.Value.MethodByName這個MethodByName,需要指定準確真實的方法名字,如果錯誤將直接panic,MethodByName返回一個函式值對應的reflect.Value方法的名字。
  3. []reflect.Value,這個是最終需要呼叫的方法的引數,可以沒有或者一個或者多個,根據實際引數來定。
  4. reflect.Value的 Call 這個方法,這個方法將最終呼叫真實的方法,引數務必保持一致,如果reflect.Value.Kind不是一個方法,那麼將直接panic。
  5. 本來可以用物件訪問方法直接呼叫的,但是如果要通過反射,那麼首先要將方法註冊,也就是MethodByName,然後通過反射呼叫methodValue.Call

反射是把雙刃劍

反射是一個強大並富有表現力的工具,能寫出更靈活的程式碼。但是反射不應該被濫用,原因有以下三個。

  1. 基於反射的程式碼是極其脆弱的,反射中的型別錯誤會在真正執行的時候才會引發panic,那很可能是在程式碼寫完的很長時間之後。
  2. 大量使用反射的程式碼通常難以理解,程式碼可讀性差。
  3. 反射的效能低下,基於反射實現的程式碼通常比正常程式碼執行速度慢一到兩個數量級。處於執行效率關鍵位置的程式碼,請避免使用反射。

相關文章