Go語言反射reflect


反射是指在程序運行期對程序本身進行訪問和修改的能力。程序在編譯時,變量被轉換為內存地址,變量名不會被編譯器寫入到可執行部分。在運行程序時,程序無法獲取自身的信息。

支持反射的語言可以在程序編譯期將變量的反射信息,如字段名稱、類型信息、結構體信息等整合到可執行文件中,並給程序提供接口訪問反射信息,這樣就可以在程序運行期獲取類型的反射信息,並且有能力修改它們。

Go程序在運行期使用reflect包訪問程序的反射信息

reflect包實現了運行時反射,允許程序操作任意類型的對象。典型用法是用靜態類型interface{}保存一個值,通過調用TypeOf獲取其動態類型信息,該函數返回一個Type類型值。調用ValueOf函數返回一個Value類型值,該值代表運行時的數據。Zero接受一個Type類型參數並返回一個代表該類型零值的Value類型值。

Go 程序的反射系統無法獲取到一個可執行文件空間中或者是一個包中的所有類型信息,需要配合使用標准庫中對應的詞法、語法解析器和抽象語法樹(AST)對源碼進行掃描后獲得這些信息。

通過反射獲取類型信息

先上圖,有了這張圖再看代碼的時候就會感覺很好理解了。

通過反射獲取類型信息:(reflect.TypeOf()和reflect.Type)

使用 reflect.TypeOf() 函數可以獲得任意值的類型對象(reflect.Type),程序通過類型對象可以訪問任意值的類型信息。下面通過例子來理解獲取類型對象的過程:

package main

import (
	"fmt"
	"reflect"
)

type Student struct {

	Name string
	Age  int
}

func main() {

	var stu Student

	typeOfStu := reflect.TypeOf(stu)

	fmt.Println(typeOfStu.Name(), typeOfStu.Kind())
}

代碼輸出如下:

Student struct

代碼說明如下:

  • 第16行,定義一個int類型的變量
  • 第18行,通過reflect.TypeOf()取得變量stu的類型對象typeOfStu,類型為reflect.Type
  • 第20行中,通過typeOfStu類型對象的成員函數,可以分別獲取到 typeOfStu 變量的類型名為 Student,種類(Kind)為 struct。

理解反射的類型(Type)與種類(Kind)

在使用反射時,需要首先理解類型(Type)和種類(Kind)的區別。編程中,使用最多的是類型,但在反射中,當需要區分一個大品種的類型時,就會用到種類(Kind)。例如,需要統一判斷類型中的指針時,使用種類(Kind)信息就較為方便。

反射種類(Kind)的定義

Go 程序中的類型(Type)指的是系統原生數據類型,如 int、string、bool、float32 等類型,以及使用 type 關鍵字定義的類型,這些類型的名稱就是其類型本身的名稱。例如使用 type A struct{} 定義結構體時,A 就是 struct{} 的類型。

種類(Kind)指的是對象歸屬的品種,在 reflect 包中有如下定義:

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						 // 32位復數類型
	Complex128					 // 64位復數類型
	Array								 // 數組
	Chan								 // 通道
	Func								 // 函數
	Interface						 // 接口
	Map								   // 映射
	Ptr								   // 指針
	Slice								 // 切片
	String							 // 字符串
	Struct							 // 結構體
	UnsafePointer				 // 底層指針
)

Map、Slice、Chan 屬於引用類型,使用起來類似於指針,但是在種類常量定義中仍然屬於獨立的種類,不屬於 Ptr。

type A struct{} 定義的結構體屬於 Struct 種類,*A 屬於 Ptr。

從類型對象中獲取類型名稱和種類的例子

Go 語言中的類型名稱對應的反射獲取方法是 reflect.Type 中的 Name() 方法,返回表示類型名稱的字符串。

類型歸屬的種類(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 類型的常量。

下面的代碼中會對常量和結構體進行類型信息獲取。

package main

import (
	"fmt"
	"reflect"
)
//定義一個Enum類型
type Enum int
const (
	Zero Enum = 0
)
type Student struct {

	Name string
	Age  int
}

func main() {

	//定義一個Student類型的變量
	var stu Student

	//獲取結構體實例的反射類型對象
	typeOfStu := reflect.TypeOf(stu)

	//顯示反射類型對象的名稱和種類
	fmt.Println(typeOfStu.Name(), typeOfStu.Kind())

	//獲取Zero常量的反射類型對象
	typeOfZero := reflect.TypeOf(Zero)

	//顯示反射類型對象的名稱和種類
	fmt.Println(typeOfZero.Name(), typeOfZero.Kind())
}

代碼輸出如下:

Student struct
Enum int

代碼說明如下:

  • 第21行,將 Student 實例化,並且使用 reflect.TypeOf() 獲取被實例化后的 Student 的反射類型對象。
  • 第27行,輸出Student的類型名稱和種類,類型名稱就是 Student,而 Student 屬於一種結構體種類,因此種類為 struct。
  • 第30行,Zero 是一個 Enum 類型的常量。這個 Enum 類型在第 9 行聲明,第 12 行聲明了常量。如沒有常量也不能創建實例,通過 reflect.TypeOf() 直接獲取反射類型對象。
  • 第33行,輸出 Zero 對應的類型對象的類型名和種類。

reflect.Elem() - 通過反射獲取指針指向的元素類型

通過反射獲取指針指向的元素類型:reflect.Elem()

Go 程序中對指針獲取反射對象時,可以通過 reflect.Elem() 方法獲取這個指針指向的元素類型。這個獲取過程被稱為取元素,等效於對指針類型變量做了一個*操作,代碼如下:

package main

import (
	"fmt"
	"reflect"
)

type Student struct {

	Name string
	Age  int
}

func main() {

	//定義一個Student類型的指針變量
	var stu = &Student{Name:"kitty", Age: 20}

	//獲取結構體實例的反射類型對象
	typeOfStu := reflect.TypeOf(stu)

	//顯示反射類型對象的名稱和種類
	fmt.Printf("name: '%v', kind: '%v'\n", typeOfStu.Name(), typeOfStu.Kind())

	//取類型的元素
	typeOfStu = typeOfStu.Elem()

	//顯示反射類型對象的名稱和種類
	fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfStu.Name(), typeOfStu.Kind())
}

代碼輸出如下:

name: '', kind: 'ptr'
element name: 'Student', element kind: 'struct'

代碼說明如下:

  • 第17行,創建了一個Student結構體的實例,stu是一個*Student的指針變量
  • 第20行,對指針變量獲取反射類型信息。
  • 第23行,輸出指針變量的類型名稱和種類。Go語言的反射中對所有指針變量的種類都是 Ptr,但注意,指針變量的類型名稱是空,不是 *Student。
  • 第26行,取指針類型的元素類型,也就是 Student 類型。這個操作不可逆,不可以通過一個非指針類型獲取它的指針類型。
  • 第29行,輸出指針變量指向元素的類型名稱和種類,得到了 Student 的類型名稱(Student)和種類(struct)。

通過反射獲取結構體的成員類型

任意值通過 reflect.TypeOf() 獲得反射對象信息后,如果它的類型是結構體,可以通過反射值對象(reflect.Type)的 NumField() 和 Field() 方法獲得結構體成員的詳細信息。與成員獲取相關的 reflect.Type 的方法如下表所示。

結構體成員訪問的方法列表
方法 說明
Field(i int) StructField 根據索引,返回索引對應的結構體字段的信息。當值不是結構體或索引超界時發生宕機
NumField() int 返回結構體成員字段數量。當類型不是結構體或索引超界時發生宕機
FieldByName(name string) (StructField, bool) 根據給定字符串返回字符串對應的結構體字段的信息。沒有找到時 bool 返回 false,當類型不是結構體或索引超界時發生宕機
FieldByIndex(index []int) StructField 多層成員訪問時,根據 []int 提供的每個結構體的字段索引,返回字段的信息。沒有找到時返回零值。當類型不是結構體或索引超界時 發生宕機
FieldByNameFunc( match func(string) bool) (StructField,bool) 根據匹配函數匹配需要的字段。當值不是結構體或索引超界時發生宕機

結構體字段類型

Type 的 Field() 方法返回 StructField 結構,這個結構描述結構體的成員信息,通過這個信息可以獲取成員與結構體的關系,如偏移、索引、是否為匿名字段、結構體標簽(Struct Tag)等,而且還可以通過 StructField 的 Type 字段進一步獲取結構體成員的類型信息。StructField 的結構如下:

type StructField struct {
    Name string          // 字段名
    PkgPath string       // 字段路徑
    Type      Type       // 字段反射類型對象
    Tag       StructTag  // 字段的結構體標簽
    Offset    uintptr    // 字段在結構體中的相對偏移
    Index     []int      // Type.FieldByIndex中的返回的索引值
    Anonymous bool       // 是否為匿名字段
}

字段說明如下。

  • Name:為字段名稱。
  • PkgPath:字段在結構體中的路徑。
  • Type:字段本身的反射類型對象,類型為 reflect.Type,可以進一步獲取字段的類型信息。
  • Tag:結構體標簽,為結構體字段標簽的額外信息,可以單獨提取。
  • Index:FieldByIndex 中的索引順序。
  • Anonymous:表示該字段是否為匿名字段。

獲取成員反射信息

下面代碼中,實例化一個結構體並遍歷其結構體成員,再通過 reflect.Type 的 FieldByName() 方法查找結構體中指定名稱的字段,直接獲取其類型信息。

反射訪問結構體成員類型及信息:

package main

import (
    "fmt"
    "reflect"
)

func main() {
  
    // 聲明一個空結構體
    type cat struct {
        Name string
      
        // 帶有結構體tag的字段
        Type int `json:"type" id:"100"`
    }
  
    // 創建cat的實例
    ins := cat{Name: "mimi", Type: 1}
  
    // 獲取結構體實例的反射類型對象
    typeOfCat := reflect.TypeOf(ins)
  
    // 遍歷結構體所有成員
    for i := 0; i < typeOfCat.NumField(); i++ {
      
        // 獲取每個成員的結構體字段類型
        fieldType := typeOfCat.Field(i)
      
        // 輸出成員名和tag
        fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
    }
  
    // 通過字段名, 找到字段類型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
      
        // 從tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

代碼輸出如下:

name: Name  tag: ''
name: Type  tag: 'json:"type" id:"100"'
type 100

代碼說明如下:

  • 第 11 行,聲明了帶有兩個成員的 cat 結構體。
  • 第 15 行,Type 是 cat 的一個成員,這個成員類型后面帶有一個以```開始和結尾的字符串。這個字符串在 Go語言中被稱為 Tag(標簽)。一般用於給字段添加自定義信息,方便其他模塊根據信息進行不同功能的處理。
  • 第 19 行,創建 cat 實例,並對兩個字段賦值。結構體標簽屬於類型信息,無須且不能賦值。
  • 第 22 行,獲取實例的反射類型對象。
  • 第 25 行,使用 reflect.Type 類型的 NumField() 方法獲得一個結構體類型共有多少個字段。如果類型不是結構體,將會觸發宕機錯誤。
  • 第 28 行,reflect.Type 中的 Field() 方法和 NumField 一般都是配對使用,用來實現結構體成員的遍歷操作。
  • 第 31 行,使用 reflect.Type 的 Field() 方法返回的結構不再是 reflect.Type 而是StructField 結構體。
  • 第 35 行,使用 reflect.Type 的 FieldByName() 根據字段名查找結構體字段信息,cat Type 表示返回的結構體字段信息,類型為 StructField,ok 表示是否找到結構體字段的信息。
  • 第 38 行中,使用 StructField 中 Tag 的 Get() 方法,根據 Tag 中的名字進行信息獲取。

通過反射獲取值信息

反射不僅可以獲取值的類型信息,還可以動態地獲取或者設置變量的值。Go語言中使用 reflect.Value 獲取和設置變量的值。

變量、interface{}和reflect.Value是可以相互轉換的。這點在實際開發中,會經常碰到。

使用反射值對象包裝任意值

Go 語言中,使用 reflect.ValueOf() 函數獲得值的反射值對象(reflect.Value)。書寫格式如下:

rValue := reflect.ValueOf(rawValue)

reflect.ValueOf 返回 reflect.Value 類型,包含有 rawValue 的值信息。reflect.Value 與原值間可以通過值包裝和值獲取互相轉化。reflect.Value 是一些反射操作的重要類型,如反射調用函數。

從反射值對象獲取被包裝的值

Go 語言中可以通過 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 將值以字符串類型返回

從反射值對象(reflect.Value)中獲取值得例子

下面代碼中,將整型變量中的值使用 reflect.Value 獲取反射值對象(reflect.Value)。再通過 reflect.Value 的 Interface() 方法獲得 interface{} 類型的原值,通過 int 類型對應的 reflect.Value 的 Int() 方法獲得整型值。

package main

import (
	"fmt"
	"reflect"
)

func main() {

	//聲明整型變量a並賦初值
	var a int = 1024

	//獲取變量a的反射值對象
	valueOfA := reflect.ValueOf(a)

	//獲取interface{}類型的值,通過類型斷言轉換
	var getA int = valueOfA.Interface().(int)

	//獲取64位的值,強制類型轉換為int類型
	var getB int = int(valueOfA.Int())

	fmt.Println(getA, getB)
}

代碼輸出如下:

1024 1024

代碼說明如下:

  • 第 11 行,聲明一個變量,類型為 int,設置初值為 1024。
  • 第 14 行,獲取變量 a 的反射值對象,類型為 reflect.Value,這個過程和 reflect.TypeOf() 類似。
  • 第 17 行,將 valueOfA 反射值對象以 interface{} 類型取出,通過類型斷言轉換為 int 類型並賦值給 getA。
  • 第 20 行,將 valueOfA 反射值對象通過 Int 方法,以 int64 類型取出,通過強制類型轉換,轉換為原本的 int 類型。

通過反射訪問結構體成員的值

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

方 法 備 注
Field(i int) Value 根據索引,返回索引對應的結構體成員字段的反射值對象。當值不是結構體或索引超界時發生宕機
NumField() int 返回結構體成員字段數量。當值不是結構體或索引超界時發生宕機
FieldByName(name string) Value 根據給定字符串返回字符串對應的結構體字段。沒有找到時返回零值,當值不是結構體或索引超界時發生宕機
FieldByIndex(index []int) Value 多層成員訪問時,根據 []int 提供的每個結構體的字段索引,返回字段的值。 沒有找到時返回零值,當值不是結構體或索引超界時發生宕機
FieldByNameFunc(match func(string) bool) Value 根據匹配函數匹配需要的字段。找到時返回零值,當值不是結構體或索引超界時發生宕機

下面代碼構造一個結構體包含不同類型的成員。通過 reflect.Value 提供的成員訪問函數,可以獲得結構體值的各種數據。

反射訪問結構體成員的值:

package main

import (
	"fmt"
	"reflect"
)

//定義結構體
type Student struct {
	Name string
	Age  int

	//嵌入字段
	float32
	bool


	next *Student
}

func main() {

	//值包裝結構體
	rValue := reflect.ValueOf(Student{
		next: &Student{},
	})

	//獲取字段數量
	fmt.Println("NumField:", rValue.NumField())

	//獲取索引為2的字段(float32字段)
	//注:經過測試發現Field(i)的參數索引是從0開始的,
	//並且是按照定義的結構體的順序來的,而不是按照字段名字的ASCii碼值來的
	floatField := rValue.Field(2)

	//輸出字段類型
	fmt.Println("Field:", floatField.Type())

	//根據名字查找字段
	fmt.Println("FieldByName(\"Age\").Type:", rValue.FieldByName("Age").Type())

	//根據索引查找值中next字段的int字段的值
	fmt.Println("FieldByIndex([]int{4, 0}).Type()", rValue.FieldByIndex([]int{4, 0}).Type())

}

輸出結果為:

NumField: 5
Field: float32
FieldByName("Age").Type: int
FieldByIndex([]int{4, 0}).Type() string

代碼說明如下:

  • 第 9 行,定義結構體,結構體的每個字段的類型都不一樣。
  • 第 24 行,實例化結構體並包裝為 reflect.Value 類型,成員中包含一個 *Student 的實例。
  • 第 29 行,獲取結構體的字段數量。
  • 第 34 和 37 行,獲取索引為2的字段值(float32 字段),並且打印類型。
  • 第 39 行,根據Age字符串,查找到 Age 字段的類型。
  • 第 41 行,[]int{4,0} 中的 4 表示,在 Student 結構中索引值為 4 的成員,也就是 next。next 的類型為 Student,也是一個結構體,因此使用 []int{4,0} 中的 0 繼續在 next 值的基礎上索引,結構為 Student 中索引值為 0 的 Name 字段,類型為 string。

判斷反射值得空和有效性

IsNil()和IsValid() -- 判斷反射值的空和有效性

反射值對象(reflect.Value)提供一系列方法進行零值和空判定,如下表所示。

反射值對象的零值和有效性判斷方法
方 法 說 明
IsNil() bool 返回值是否為 nil。如果值類型不是通道(channel)、函數、接口、map、指針或 切片時發生 panic,類似於語言層的v== nil操作
IsValid() bool 判斷值是否有效。 當值本身非法時,返回 false,例如 reflect Value不包含任何值,值為 nil 等。

下面的例子將會對各種方式的空指針進行 IsNil() 和 IsValid() 的返回值判定檢測。同時對結構體成員及方法查找 map 鍵值對的返回值進行 IsValid() 判定,參考下面的代碼。

反射值對象的零值和有效性判斷:

package main

import (
	"fmt"
	"reflect"
)

func main() {

	//*int的空指針
	var a *int
	fmt.Println("var a *int:", reflect.ValueOf(a).IsNil())

	//nil值
	fmt.Println("nil:", reflect.ValueOf(nil).IsValid())

	//*int類型的空指針
	fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())

	//實例化一個結構體
	s := struct {}{}

	//嘗試從結構體中查找一個不存在的字段
	fmt.Println("不存在的結構體成員:", reflect.ValueOf(s).FieldByName("").IsValid())

	//嘗試從結構體中查找一個不存在的方法
	fmt.Println("不存在的方法:", reflect.ValueOf(s).MethodByName("").IsValid())

	//實例化一個map
	m := map[int]int{}

	//嘗試從map中查找一個不存在的鍵
	fmt.Println("不存在的鍵:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}

輸出結果:

var a *int: true
nil: false
(*int)(nil): false
不存在的結構體成員: false
不存在的方法: false
不存在的鍵: false

代碼說明如下:

  • 第 11 行,聲明一個 *int 類型的指針,初始值為 nil。
  • 第 12 行,將變量 a 包裝為 reflect.Value 並且判斷是否為空,此時變量 a 為空指針,因此返回 true。
  • 第 15 行,對 nil 進行 IsValid() 判定(有效性判定),返回 false。
  • 第 18 行,(*int)(nil) 的含義是將 nil 轉換為 int,也就是int 類型的空指針。此行將 nil 轉換為 int 類型,並取指針指向元素。由於 nil 不指向任何元素,int 類型的 nil 也不能指向任何元素,值不是有效的。因此這個反射值使用 Isvalid() 判斷時返回 false。
  • 第 21 行,實例化一個結構體。
  • 第 24 行,通過 FieldByName 查找 s 結構體中一個空字符串的成員,如成員不存在,IsValid() 返回 false。
  • 第 27 行,通過 MethodByName 查找 s 結構體中一個空字符串的方法,如方法不存在,IsValid() 返回 false。
  • 第 30 行,實例化一個 map,這種寫法與 make 方式創建的 map 等效。
  • 第 33 行,MapIndex() 方法能根據給定的 reflect.Value 類型的值查找 map,並且返回查找到的結果。

IsNil() 常被用於判斷指針是否為空;IsValid() 常被用於判定返回值是否有效。

通過反射修改變量的值

使用 reflect.Value 對包裝的值進行修改時,需要遵循一些規則。如果沒有按照規則進行代碼設計和編寫,輕則無法修改對象值,重則程序在運行時會發生宕機。

判斷及獲取元素的相關方法

使用 reflect.Value 取元素、取地址及修改值的屬性方法請參考下表。

反射值對象的判定及獲取元素的方法
方法名 備 注
Elem() Value 取值指向的元素值,類似於語言層*操作。當值類型不是指針或接口時發生宕 機,空指針時返回 nil 的 Value
Addr() Value 對可尋址的值返回其地址,類似於語言層&操作。當值不可尋址時發生宕機
CanAddr() bool 表示值是否可尋址
CanSet() bool 返回值能否被修改。要求值可尋址且是導出的字段

值修改相關方法

使用 reflect.Value 修改值的相關方法如下表所示。

反射值對象修改值的方法
Set(x Value) 將值設置為傳入的反射值對象的值
Setlnt(x int64) 使用 int64 設置值。當值的類型不是 int、int8、int16、 int32、int64 時會發生宕機
SetUint(x uint64) 使用 uint64 設置值。當值的類型不是 uint、uint8、uint16、uint32、uint64 時會發生宕機
SetFloat(x float64) 使用 float64 設置值。當值的類型不是 float32、float64 時會發生宕機
SetBool(x bool) 使用 bool 設置值。當值的類型不是 bod 時會發生宕機
SetBytes(x []byte) 設置字節數組 []bytes值。當值的類型不是 []byte 時會發生宕機
SetString(x string) 設置字符串值。當值的類型不是 string 時會發生宕機

以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值時會發生宕機。

在已知值的類型時,應盡量使用值對應類型的反射設置值。

值可修改條件之一:可被尋址

通過反射修改變量值的前提條件之一:這個值必須可以被尋址。簡單地說就是這個變量必須能被修改。示例代碼如下:

package main

import "reflect"

func main() {

	//聲明整形變量a並賦初值
	var a int = 1024

	//獲取變量a的反射值對象
	rValue := reflect.ValueOf(a)

	//嘗試將a修改為1(此處會崩潰)
	rValue.SetInt(1)
}

程序運行崩潰,打印錯誤

panic: reflect: reflect.Value.SetInt using unaddressable value

報錯意思是:SetInt正在使用一個不能被尋址的值。從 reflect.ValueOf 傳入的是 a 的值,而不是 a 的地址,這個 reflect.Value 當然是不能被尋址的。將代碼修改一下,重新運行:

package main

import (
	"fmt"
	"reflect"
)

func main() {

	//聲明整形變量a並賦初值
	var a int = 1024

	//獲取變量a的反射值對象
	rValue := reflect.ValueOf(&a)

	//取出a地址的元素(a的值)
	rValue = rValue.Elem()

	//嘗試將a修改為1
	rValue.SetInt(1)

	//打印a的值
	fmt.Println(rValue.Int())
}

代碼輸出

1

下面是對代碼的分析:

  • 第 14 行中,將變量 a 取值后傳給 reflect.ValueOf()。此時 reflect.ValueOf() 返回的 valueOfA 持有變量 a 的地址。
  • 第 17 行中,使用 reflect.Value 類型的 Elem() 方法獲取 a 地址的元素,也就是 a 的值。reflect.Value 的 Elem() 方法返回的值類型也是 reflect.Value。
  • 第 20 行,此時 rValue 表示的是 a 的值且可以尋址。使用 SetInt() 方法設置值時不再發生崩潰。
  • 第 23 行,正確打印修改的值。

提示

當 reflect.Value 不可尋址時,使用 Addr() 方法也是無法取到值的地址的,同時會發生宕機。雖然說 reflect.Value 的 Addr() 方法類似於語言層的&操作;Elem() 方法類似於語言層的*操作,但並不代表這些方法與語言層操作等效。

值可修改條件之一:被導出

結構體成員中,如果字段沒有被導出,即便不使用反射也可以被訪問,但不能通過反射修改,代碼如下:

package main

import "reflect"

func main() {

	type dog struct {
		legCount int
	}

	//獲取dog實例的反射值對象
	valueOfDog := reflect.ValueOf(&dog{})

	valueOfDog = valueOfDog.Elem()

	//獲取legCount字段的值
	vLegCount := valueOfDog.FieldByName("legCount")

	//嘗試設置legCount的值(這里會發生崩潰)
	vLegCount.SetInt(4)
}

程序發生崩潰,報錯:

panic: reflect: reflect.Value.SetInt using value obtained using unexported field

報錯的意思是:SetInt() 使用的值來自於一個未導出的字段。

為了能修改這個值,需要將該字段導出。將 dog 中的 legCount 的成員首字母大寫,導出 LegCount 讓反射可以訪問,修改后的代碼如下:

package main

import (
	"fmt"
	"reflect"
)

func main() {

	type dog struct {
		LegCount int
	}

	//獲取dog實例的反射值對象
	valueOfDog := reflect.ValueOf(&dog{})

  //// 取出dog實例地址的元素
	valueOfDog = valueOfDog.Elem()

	//獲取legCount字段的值
	vLegCount := valueOfDog.FieldByName("LegCount")

	//嘗試設置legCount的值
	vLegCount.SetInt(4)

	fmt.Println(vLegCount.Int())
}

代碼輸出如下:

4

代碼說明如下:

  • 第 10 行,將 LegCount 首字母大寫導出該字段。
  • 第 15 行,獲取 dog 實例指針的反射值對象。
  • 第 19 行,取 dog 實例的指針元素,也就是 dog 的實例。
  • 第 21 行,取 dog 結構體中 LegCount 字段的成員值。
  • 第 24 行,修改該成員值。
  • 第 26 行,打印該成員值。

值的修改從表面意義上叫可尋址,換一種說法就是值必須“可被設置”。那么,想修改變量值,一般的步驟是:

  1. 取這個變量的地址或者這個變量所在的結構體已經是指針類型。
  2. 使用 reflect.ValueOf 進行值包裝。
  3. 通過 Value.Elem() 獲得指針值指向的元素值對象(Value),因為值對象(Value)內部對象為指針時,使用 set 設置時會報出宕機錯誤。
  4. 使用 Value.SetXXX 設置值。

通過類型信息創建實例

當已知 reflect.Type 時,可以動態地創建這個類型的實例,實例的類型為指針。例如 reflect.Type 的類型為 int 時,創建 int 的指針,即*int,代碼如下:

package main

import (
	"fmt"
	"reflect"
)

func main() {

	var a int

	//取變量a的反射類型對象
	typeOfA := reflect.TypeOf(a)

	//根據反射類型對象創建類型實例
	aIns := reflect.New(typeOfA)

	//輸出Value的類型和種類
	fmt.Println(aIns.Type(), aIns.Kind())
}

代碼輸出結果如下

*int ptr

代碼說明如下:

  • 第 13 行,獲取變量 a 的反射類型對象。
  • 第 16 行,使用 reflect.New() 函數傳入變量 a 的反射類型對象,創建這個類型的實例值,值以 reflect.Value 類型返回。這步操作等效於:new(int),因此返回的是 *int 類型的實例。
  • 第 19 行,打印 aIns 的類型為 *int,種類為指針。

通過反射調用函數

如果反射值對象(reflect.Value)中值的類型為函數時,可以通過 reflect.Value 調用該函數。使用反射調用函數時,需要將參數使用反射值對象的切片 []reflect.Value 構造后傳入 Call() 方法中,調用完成時,函數的返回值通過 []reflect.Value 返回。

下面的代碼聲明一個加法函數,傳入兩個整型值,返回兩個整型值的和。將函數保存到反射值對象(reflect.Value)中,然后將兩個整型值構造為反射值對象的切片([]reflect.Value),使用 Call() 方法進行調用。

反射調用函數:

package main

import (
	"fmt"
	"reflect"
)

//普通函數
func add(a, b int) int {
	return a + b
}

func main() {

	//將函數包裝為反射值對象
	funcValue := reflect.ValueOf(add)

	//構造函數參數,傳入兩個整形值
	paramList := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)}

	//反射調用函數
	retList := funcValue.Call(paramList)

	fmt.Println(retList[0].Int())
}

代碼說明如下:

  • 第 9~12 行,定義一個普通的加法函數。
  • 第 17 行,將 add 函數包裝為反射值對象。
  • 第 20 行,將 10 和 20 兩個整型值使用 reflect.ValueOf 包裝為 reflect.Value,再將反射值對象的切片 []reflect.Value 作為函數的參數。
  • 第 23 行,使用 funcValue 函數值對象的 Call() 方法,傳入參數列表 paramList 調用 add() 函數。
  • 第 26 行,調用成功后,通過 retList[0] 取返回值的第一個參數,使用 Int 取返回值的整數值。

提示

反射調用函數的過程需要構造大量的 reflect.Value 和中間變量,對函數參數值進行逐一檢查,還需要將調用參數復制到調用函數的參數內存中。調用完畢后,還需要將返回值轉換為 reflect.Value,用戶還需要從中取出調用值。因此,反射調用函數的性能問題尤為突出,不建議大量使用反射函數調用。

通過反射調用方法

調用方法和調用函數是一樣的,只不過結構體需要先通過rValue.Method()先獲取方法再調用,請看如下示例:

package main

import (
	"fmt"
	"reflect"
)

type MyMath struct {
	Pi float64
}

//普通函數
func (myMath MyMath) Sum(a, b int) int {
	return a + b
}

func (myMath MyMath) Dec(a, b int) int {
	return a - b
}

func main() {

	var myMath = MyMath{Pi:3.14159}

	//獲取myMath的值對象
	rValue := reflect.ValueOf(myMath)

	//獲取到該結構體有多少個方法
	//numOfMethod := rValue.NumMethod()

	//構造函數參數,傳入兩個整形值
	paramList := []reflect.Value{reflect.ValueOf(30), reflect.ValueOf(20)}


	//調用結構體的第一個方法Method(0)
	//注意:在反射值對象中方法索引的順序並不是結構體方法定義的先后順序
	//而是根據方法的ASCII碼值來從小到大排序,所以Dec排在第一個,也就是Method(0)
	result := rValue.Method(0).Call(paramList)

	fmt.Println(result[0].Int())
	
}

代碼輸出結果為:

10


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM