反射 - Go 語言學習筆記

前言

在計算機科學中,反射是指計算機程序在運行時(Run time)能夠訪問、檢測和修改它自己狀態或行爲的一種能力。用比喻來講,反射就是程序在運行的時候可以「觀察」而且修改本身的行爲。bash

什麼是 Go 的反射

Go 語言提供了一種機制在運行時更新變量和檢查它們的值、調用它們的方法,可是在編譯時並不知道這些變量的具體類型,這稱爲反射機制(refletion)。函數

Go 語言官方自帶的 reflect 包就是實現反射相關的,reflect 包定義了各類類型,實現了反射的各類函數,經過它們能夠在運行時檢測類型的信息、改變類型的值。工具

爲何須要反射

須要反射的 2 個常見場景:佈局

  1. 若是要編寫一個函數,用於處理通用類型的值,而這些類型可能沒法共享同一個接口,也可能佈局未知,也有可能這個類型在咱們設計函數時還不存在,甚至這個類型會同時存在這三種問題,這時,反射是最好的解決方案。
  2. 有時候須要根據某些條件決定調用哪一個函數,好比根據用戶的輸入來決定。這時就須要對函數和函數的參數進行反射,在運行期間動態地執行函數。

Go 語言的 fmt.Printf 函數中的格式化邏輯就是使用反射處理相似以上存在的問題來實現的。測試

下面嘗試實現一個相似 fmt.Printf 功能的函數,爲了簡單起見,函數只接收一個參數,而後返回和 fmt.Sprint 相似的格式化後的字符串,函數名也叫 Sprint。ui

首先用 switch 類型分支來測試輸入參數是否實現了 String 方法,若是是就調用該方法。而後繼續增長類型測試分支,檢查這個值的動態類型是不是 string、int、bool 等基礎類型,並在每種狀況下執行相應的格式化操做。url

func Sprint(x interface{}) string {
    type stringer interface {
        String() string
    }
    switch x := x.(type) {
    case stringer:
        return x.String()
    case string:
        return x
    case int:
        return strconv.Itoa(x)
    // ...similar cases for int16, uint32, and so on...
    case bool:
        if x {
            return "true"
        }
        return "false"
    default:
        // array, chan, func, map, pointer, slice, struct
        return "???"
    }
}
複製代碼

以上函數雖然實現了部分類型的格式化輸出,可是如何處理其它相似 []float6四、map[string][]string 等類型呢?固然能夠添加更多的測試分支,可是這些組合類型的數目基本是無窮的。還有如何處理相似url.Values這樣的具名類型呢?即便類型分支能夠識別出底層的基礎類型是 map[string][]string,可是它並不匹配 url.Values 類型,由於它們是兩種不一樣的類型,並且 switch 類型分支也不可能包含每一個相似 url.Values 的類型,這會致使對這些庫的依賴。spa

沒有辦法來檢查未知類型的表示方式,被卡住了。這就是爲什麼須要反射的緣由。設計

反射是如何實現的

interface 是 Go 語言實現抽象的一個很是強大的工具。當向接口變量賦予一個實體類型的時候,接口會存儲實體的類型信息,反射就是經過接口的類型信息實現的,反射創建在類型的基礎上。指針

types 和 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 語言聲明變量時指定的 type 是 static type,在建立變量的時候就已經肯定。反射主要與 Go 語言的 interface 類型相關(它的 type 是 concrete type ),只有 interface 類型纔有反射一說。

Go 語言中,每一個 interface 變量都有一個對應 pair,pair 中記錄了實際變量的值和類型:

(value, type)
複製代碼

以上,value 是實際變量值,type 是實際變量的類型。一個 interface{} 類型的變量包含了2個指針,一個指針指向值的類型【對應 concrete type】,另一個指針指向實際的值【對應 value】。

Go 語言的 reflect 包

Go 語言的反射功能由 reflect 包提供,它實現了運行時反射,使用它能識別 interface{} 變量的底層具體類型和具體值。

1. reflect.Type 和 reflect.Value
reflect 包定義了兩個重要的類型:Type 和 Value。reflect.Type 表示 interface{} 的具體類型,而 reflect.Value 表示它的具體值。reflect.TypeOf() 和 reflect.ValueOf() 兩個函數能夠分別返回 reflect.Type 和 reflect.Value。

  • TypeOf 用來動態獲取輸入參數接口中的值的類型,若是接口爲空則返回nil
  • ValueOf用來獲取輸入參數接口中的數據的值,若是接口爲空則返回0

即,reflect.TypeOf() 是獲取 pair 中的 type,reflect.ValueOf() 獲取 pair 中的value,示例以下:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345

	fmt.Println("type: ", reflect.TypeOf(num))
	fmt.Println("value: ", reflect.ValueOf(num))
}
複製代碼

運行結果:

type:  float64
value:  1.2345
複製代碼

2. relfect.Kind
reflect 包中還有一個重要的類型:Kind。 在反射包中,Kind 和 Type 的類型可能看起來很類似,但在下面程序中,能夠很清楚地看出它們的不一樣之處。

package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type ", t)
    fmt.Println("Kind ", k)


}

func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
}
複製代碼

輸出:

Type  main.order
Kind  struct
複製代碼

由上可知:Type 表示 interface{} 的實際類型(在這裏是 main.Order),而 Kind 表示該類型的特定類別(在這裏是 struct)。

反射的使用

1. 從 relfect.Value 中獲取接口 interface 的信息

當執行 reflect.ValueOf(interface) 以後,就獲得了一個類型爲 「relfect.Value」 變量,能夠經過它自己的 Interface() 方法得到接口變量的真實內容,而後能夠經過類型判斷進行轉換,轉換爲原有真實類型。

  • 已知原有類型【進行「強制轉換」】
    已知類型後轉換爲其對應的類型的作法以下,直接經過 Interface 方法而後強制轉換,以下:
realValue := value.Interface().(已知的類型)
複製代碼

示例以下:

package main

import (
	"fmt"
	"reflect"
)

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)
}
複製代碼

運行結果:

0xc42000e238
1.2345
複製代碼

說明:

  1. 轉換的時候,若是轉換的類型不徹底符合,則直接panic,類型要求很是嚴格!
  2. 轉換的時候,要區分是指針仍是指
  3. 也就是說反射能夠將「反射類型對象」再從新轉換爲「接口類型變量」
  • 未知原有類型【遍歷探測其Filed】】
    不少狀況下,可能並不知道其具體類型,那麼這個時候,須要進行遍歷探測其 Filed 來得知,示例以下:
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func main() {

	user := User{1, "Allen.Wu", 25}

	DoFiledAndMethod(user)

}

// 經過接口來獲取任意參數,而後一一揭曉
func DoFiledAndMethod(input interface{}) {

	getType := reflect.TypeOf(input)
	fmt.Println("get Type is :", getType.Name())

	getValue := reflect.ValueOf(input)
	fmt.Println("get all Fields is:", getValue)

	// 獲取方法字段
	// 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()
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 獲取方法
	// 1. 先獲取interface的reflect.Type,而後經過.NumMethod進行遍歷
	for i := 0; i < getType.NumMethod(); i++ {
		m := getType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}
複製代碼

運行結果:

get Type is : User
get all Fields is: {1 Allen.Wu 25}
Id: int = 1
Name: string = Allen.Wu
Age: int = 25
ReflectCallFunc: func(main.User)
複製代碼

說明
經過運行結果能夠得知獲取未知類型的 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 的嵌套都是同樣的判斷處理方式

2. 經過 reflect.Value 設置實際變量的值

reflect.Value 是經過 reflect.ValueOf(x) 得到的,只有當 x 是指針的時候,才能夠經過 reflec.Value 修改實際變量 x 的值,即:要修改反射類型的對象就必定要保證其值是 「addressable」 的。 示例以下:

package main

import (
	"fmt"
	"reflect"
)

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. 也就是說若是要修改反射類型對象,其值必須是「addressable」【對應的要傳入的是指針,同時要經過Elem方法獲取原始值對應的反射對象】
  6. struct 或者 struct 的嵌套都是同樣的判斷處理方式

3. 經過 reflect.ValueOf 來進行方法的調用

示例以下:

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (u User) ReflectCallFuncHasArgs(name string, age int) {
	fmt.Println("ReflectCallFuncHasArgs name: ", name, ", age:", age, "and origal User.Name:", u.Name)
}

func (u User) ReflectCallFuncNoArgs() {
	fmt.Println("ReflectCallFuncNoArgs")
}

// 如何經過反射來進行方法的調用?
// 原本能夠用u.ReflectCallFuncXXX直接調用的,可是若是要經過反射,那麼首先要將方法註冊,也就是MethodByName,而後經過反射調動mv.Call
func main() {
	user := User{1, "Allen.Wu", 25}

	// 1. 要經過反射來調用起對應的方法,必需要先經過reflect.ValueOf(interface)來獲取到reflect.Value,獲得「反射類型對象」後才能作下一步處理
	getValue := reflect.ValueOf(user)

	// 必定要指定參數爲正確的方法名
	// 2. 先看看帶有參數的調用方法
	methodValue := getValue.MethodByName("ReflectCallFuncHasArgs")
	args := []reflect.Value{reflect.ValueOf("wudebao"), reflect.ValueOf(30)}
	methodValue.Call(args)

	// 必定要指定參數爲正確的方法名
	// 3. 再看看無參數的調用方法
	methodValue = getValue.MethodByName("ReflectCallFuncNoArgs")
	args = make([]reflect.Value, 0)
	methodValue.Call(args)
}
複製代碼

運行結果:

ReflectCallFuncHasArgs name: wudebao, age: 30 and origal User.Name: Allen.Wu
ReflectCallFuncNoArgs
複製代碼

說明

  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. 原本能夠用 u.ReflectCallFuncXXX 直接調用的,可是若是要經過反射,那麼首先要將方法註冊,也就是 MethodByName,而後經過反射調用 methodValue.Call

相關文章
相關標籤/搜索