文章目錄
- 基本介紹
- reflect包
- reflect.Type
- reflect.Value
- reflect.Kind
- 具體類型、空接口與reflect.Value的相互轉換
- 反射應用場景
- 修改變量的值
- 訪問結構體的字段信息
- 調用變量所綁定的方法
- 實現函數適配器
- 創建任意類型的變量
基本介紹
基本介紹
- 在Go中,反射(reflection)是一種機制,其允許程序在運行時檢查并操作變量、類型和結構的信息,而不需要提前知道它們的具體定義,使得代碼更加靈活和通用。
- 反射通常用于動態獲取獲取類型信息、動態創建對象、動態調用函數、動態修改對象等,在實現反射時需要用到reflect包。
- 需要注意的是,雖然反射的功能強大,但由于其使用了運行時的類型檢查和動態調用,在性能上可能會有一定的開銷,因此在性能敏感的場景中,應該盡量避免過度依賴反射來實現常規的編程任務。
reflect包
reflect.Type
reflect.Type
- reflect.Type是reflect包中的一個接口類型,用于表示任意變量的類型信息。
- 通過reflect包中的TypeOf函數,可以獲取指定變量的類型信息。
reflect.TypeOf函數的函數原型如下:
func TypeOf(i interface{}) Type
reflect.Type接口中常用的方法如下:
方法名 | 功能 |
---|---|
Kind | 獲取該類型對應的Kind |
Size | 獲取該類型的大小 |
Elem | 獲取該類型的元素的Type |
NumField | 獲取結構體類型的字段數 |
NumMethod | 獲取該類型所綁定的方法數 |
Field | 獲取結構體類型的第i個字段的信息 |
Method | 獲取該類型所綁定的第i個方法的信息 |
FieldByName | 獲取結構體類型的字段中,指定字段名的字段信息 |
MethodByName | 獲取該類型所綁定的方法中,指定方法名的方法信息 |
NumIn | 獲取函數/方法類型的參數個數 |
In | 獲取函數/方法類型的第i個參數的Type |
NumOut | 獲取函數/方法類型的返回值個數 |
Out | 獲取函數/方法類型的第i個返回值的Type |
說明一下:
- reflect.Type接口中的方法不需要用戶手動實現,這些方法由反射系統在運行時為每個類型自動生成。
- reflect.Type接口中的方法不是對所有類型都能使用,每個方法都有其特定的適用范圍和前提條件,如果在不滿足調用條件的情況下調用了某個方法,則會觸發panic異常。比如Elem方法只適用于數組、channel、map、指針和切片類型,NumField和Field方法只適用于結構體類型,NumIn、In、NumOut和Out方法只適用于函數或方法類型。
字段信息
通過reflect.Type接口的Field或FieldByName方法,能夠獲取結構體中某個字段的字段信息,獲取到的字段信息通過StructField結構體進行描述。StructField結構體的定義如下:
type StructField struct {Name string // field namePkgPath string // package pathType Type // field typeTag StructTag // field tag stringOffset uintptr // offset within struct, in bytesIndex []int // index sequence for Type.FieldByIndexAnonymous bool // is an embedded field
}
字段說明:
- Name:表示該字段的名稱。
- PkgPath:表示該字段所在的包路徑,對于可導出的字段,PkgPath為空字符串。
- Type:表示該字段對應的reflect.Type。
- Tag:表示該字段的Tag標簽信息。
- Offset:表示該字段在結構體中的偏移量。
- Index:表示該字段的索引序列。
- Anonymous:表示該字段是否為匿名字段。
方法信息
通過reflect.Type接口的Method或MethodByName方法,能夠獲取對應類型所綁定的某個方法的方法信息,獲取到的方法信息通過Method結構體進行描述。Method結構體的定義如下:
type Method struct {Name string // method namePkgPath string // package pathType Type // method typeFunc Value // func with receiver as first argumentIndex int // index for Type.Method
}
字段說明:
- Name:表示該方法的名稱。
- PkgPath:表示該方法所在的包路徑,對于可導出的方法,PkgPath為空字符串。
- Type:表示該方法對應的reflect.Type。
- Func:表示該方法對應的reflect.Value。
- Index:表示該方法在對應類型的方法集中的索引。
reflect.Value
reflect.Value
- reflect.Value是reflect包中的一個類型,用于表示任意變量的值。
- 通過reflect包中的ValueOf函數,可以獲取持有指定變量的Value。
- 通過reflect包中的New函數,可以創建指定類型的變量,并獲取持有指向該變量的指針的Value。
reflect.ValueOf函數的函數原型如下:
func ValueOf(i interface{}) Value
func New(typ Type) Value
reflect.Value類型常用的方法如下:
方法名 | 功能 |
---|---|
Kind | 獲取所持有的值對應的Kind |
Type | 獲取所持有的值對應的Type |
Elem | 獲取所持有的接口保管的值的Value封裝,或獲取所持有的指針指向的值的Value封裝 |
Index | 獲取所持有的值的第i個元素的Value封裝 |
NumField | 獲取所持有的結構體類型值的字段數 |
NumMethod | 獲取所持有的值所綁定的方法數 |
Field | 獲取所持有的結構體類型值的第i個字段的Value封裝 |
Method | 獲取所持有的值所綁定的第i個方法的函數形式的Value封裝 |
FieldByName | 獲取所持有的結構體類型值的字段中,指定字段名的字段的Value封裝 |
MethodByName | 獲取所持有的值所綁定的方法中,指定方法名的方法的函數形式的Value封裝 |
Call | 指定參數調用所持有的函數,返回函數返回值的Value封裝 |
Interface | 返回所持有的值的interface{}類型值 |
Int、Float、Bool、String、Pointer | 返回所持有的值的對應類型值,如果所持有的值不是對應的類型,則會觸發panic異常 |
SetInt、SetFloat、SetBool、SetString、SetPointer | 設置所持有的值,如果所持有的值不是對應的類型,則會觸發panic異常 |
Set | 將所持有的值設置為指定Value所持有的值,指定Value所持有值的類型必須與當前所持有值的類型相同,否則會觸發panic異常 |
說明一下:
- reflect.Value類型的方法不是對所有類型都能使用,每個方法都有其特定的適用范圍和前提條件,如果在不滿足調用條件的情況下調用了某個方法,則會觸發panic異常。比如Elem方法只適用于接口和指針類型,NumField和Field方法只適用于結構體類型,Index方法只適用于數組、channel、切片和字符串類型,Call方法只適用于函數或方法類型。
- 通過Method或MethodByName方法,獲取v所持有的值所綁定的某個方法的函數形式的Value封裝時,返回值持有的函數總是使用v所持有的值作為receiver(即第一個參數),因此返回值在調用Call方法時不用手動傳入receiver參數。
reflect.Value與reflect.Type
reflect.Value是一個具體的類型,而reflect.Type被設計成了一個接口類型。其原因如下:
- 類型的信息需要反射系統在運行時為每個類型自動生成,以適應各種未知的類型,將reflect.Type定義成接口類型的目的就是,指明運行時需要為每個類型生成哪些方法。
- reflect.Value提供了各種訪問和操作所持有值的方法,在使用reflect.Value時,通常已經知道所持有值的具體類型,這時通過reflect.Type即可獲取到所持有值的類型信息,運行時不需要為其動態的生成任何方法,因此最終將reflect.Value定義成具體類型。
reflect.Kind
reflect.Kind
- reflect.Kind是reflect包中的一個類型,用于表示類型的類別。
- reflect.Type和reflect.Value都提供了對應的Kind方法,用于獲取類型的Kind。
reflect.Kind本質是一個常量枚舉類型。其定義如下:
type Kind uintconst (Invalid Kind = iotaBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64UintptrFloat32Float64Complex64Complex128ArrayChanFuncInterfaceMapPointerSliceStringStructUnsafePointer
)
說明一下:
- 類型和類別可能是一對一的,比如int類型對應的Kind是Int,float32類型對應的Kind是Float32。類型和類別也可能是一對多的,比如所有結構體類型對應的Kind都是Struct,所有指針類型對應的Kind都是Pointer。
- 在獲取變量的reflect.Value時,ValueOf函數或提取出接口值中對應的動態類型和動態值,并返回具體類型的Value。如果需要創建一個Kind為Interface的Value,可以先通過ValueOf函數獲取一個指向接口的指針的Value,然后通過Value的Elem方法獲取Value持有的指針指向的值的Value封裝,這時獲取到的Value的Kind就是Interface。
具體類型、空接口與reflect.Value的相互轉換
具體類型、空接口與reflect.Value的相互轉換
在反射過程中,變量的類型經常需要在具體類型、空接口類型和reflect.Value類型之間進行轉換。其轉換的方式如下:
- 將變量由具體類型轉換為空接口類型時,直接通過變量賦值的方式即可。
- 將變量由空接口類型轉換為reflect.Value類型時,通過調用reflect.ValueOf函數即可。
- 將變量由reflect.Value類型轉換為空接口類型時,通過調用reflect.Value的Interface方法即可。
- 將變量由空接口類型轉為具體類型時,需要借助類型斷言。
轉換示意圖如下:
轉換案例如下:
package mainimport ("fmt""reflect"
)type Student struct {Name stringAge int
}func Reflect(iVal interface{}) { // 具體類型->interface{}rVal := reflect.ValueOf(iVal) // interface{}->reflect.ValueiVal2 := rVal.Interface() // reflect.Value->interface{}switch val := iVal2.(type) { // interface{}->具體類型case Student:fmt.Printf("type = %T, value = %v\n", val, val)case int:fmt.Printf("type = %T, value = %v\n", val, val)case float64:fmt.Printf("type = %T, value = %v\n", val, val)default:fmt.Printf("unknown type: %T\n", val)}
}func main() {var stu = Student{"Alice", 14}Reflect(stu) // type = main.Student, value = {Alice 14}Reflect(1) // type = int, value = 1Reflect(1.2) // type = float64, value = 1.2
}
反射應用場景
修改變量的值
修改變量的值
通過反射可以修改變量的值,具體步驟如下:
- 通過reflect.ValueOf函數,獲取指向該值的指針的Value封裝v1。
- 通過Value的Elem方法,獲取v1所持有的指針指向的值的Value封裝v2。
- 通過Value的Set系列方法,設置v2所持有的值,完成對變量的修改。
案例如下:
package mainimport ("fmt""reflect"
)func Reflect(iVal interface{}) {rVal := reflect.ValueOf(iVal) // 獲取指向該值的指針的Value封裝switch val := iVal.(type) {case *int:rVal.Elem().SetInt(20) // 獲取所持有的指針指向的值的Value封裝,并設置所持有的值default:fmt.Printf("unknown type: %T\n", val)}
}func main() {var a = 10Reflect(&a) // 傳入的是指向變量的指針fmt.Printf("a = %d\n", a) // a = 20
}
說明一下:
- 通過反射修改變量的值時,需要通過指向對應變量的指針來修改,這樣反射內部才能找到需要被修改的變量并對其進行修改。在修改變量的值時,需要先通過Value的Elem方法獲取所持有的指針指向的值的Value封裝(可以理解成對指針解引用),然后再調用Set系列方法修改變量的值。
- 除了通過Set系列方法修改變量的值外,也可以使用Set方法將當前Value所持有的值設置為另一個Value所持有的值。
訪問結構體的字段信息
訪問結構體的字段信息
通過反射可以訪問結構體的字段信息,具體步驟如下:
- 通過reflect.ValueOf和reflect.TypeOf函數,分別獲取結構體變量的Value和Type。
- 通過Value或Type的NumField方法,獲取結構體的字段數。
- 通過Value的Field方法,獲取指定索引字段的Value。
- 通過Type的Field方法,獲取指定索引字段的各種信息。
案例如下:
package mainimport ("fmt""reflect"
)type Student struct {Name string `json:"name"`Age int `json:"age"`
}func Reflect(iVal interface{}) {rVal := reflect.ValueOf(iVal)rType := reflect.TypeOf(iVal)rKind := rType.Kind()if rKind != reflect.Struct { // 確保傳入的變量是結構體類型return}// 訪問結構體的字段信息num := rType.NumField() // 獲取結構體的字段數for i := 0; i < num; i++ {fieldInfo := rType.Field(i) // 獲取結構體第i個字段的信息filedValue := rVal.Field(i) // 獲取結構體第i個字段的Value封裝fmt.Printf("field[%d] name = %s\ttype = %v\ttag = %s\tvalue = %v\n",i, fieldInfo.Name, fieldInfo.Type, fieldInfo.Tag, filedValue)}
}func main() {var stu = Student{"Alice", 14}Reflect(stu)
}
程序的運行結果如下:
說明一下:
- 上述代碼中通過對變量的Kind進行判斷,以確保傳入的變量是結構體類型。
- json.Marshal函數在對結構體變量進行JSON序列化時,在函數內部就是通過反射來獲取結構體字段的Tag標簽的。
調用變量所綁定的方法
調用變量所綁定的方法
通過反射可以調用變量所綁定的方法,具體步驟如下:
- 通過reflect.ValueOf和reflect.TypeOf函數,分別獲取變量的Value和Type。
- 通過Value或Type的NumMethod方法,獲取變量對應的類型所綁定的方法數。
- 通過Value的Method方法,獲取指定索引方法的Value。
- 通過Type的Method方法,獲取指定索引方法的各種信息。
- 通過Value的Call方法,調用所持有的方法。
案例如下:
package mainimport ("fmt""reflect"
)type Student struct {Name string `json:"name"`Age int `json:"age"`
}func (stu Student) Study() {fmt.Printf("Study: student %s is studying...\n", stu.Name)
}func (stu *Student) UpdateAge(age int) {stu.Age = agefmt.Printf("UpdateAge: update %s age = %d...\n", stu.Name, stu.Age)
}func (stu Student) StuInfo() {fmt.Printf("StuInfo: name = %s, age = %d...\n", stu.Name, stu.Age)
}func Reflect(iVal interface{}) {rVal := reflect.ValueOf(iVal)rType := reflect.TypeOf(iVal)switch val := iVal.(type) {case *Student, Student:fmt.Printf("------type = %v------\n", rType)// 調用變量對應的類型所綁定的方法num := rType.NumMethod() // 獲取該類型所綁定的方法數fmt.Printf("method num = %d\n", num)for i := 0; i < num; i++ {methodVal := rVal.Method(i) // 獲取該類型所綁定的第i個方法的Value封裝methodInfo := rType.Method(i) // 獲取該類型所綁定的第i個方法的信息if methodInfo.Name == "UpdateAge" { // 調用時需要傳參var args []reflect.Valueargs = append(args, reflect.ValueOf(18))methodVal.Call(args) // 調用方法} else {methodVal.Call(nil) // 調用方法}}default:fmt.Printf("unknown type: %T\n", val)}
}func main() {var stu1 = Student{"Alice", 14}Reflect(&stu1)fmt.Printf("stu1 = %v\n", stu1) // stu1 = {Alice 18}var stu2 = Student{"Bob", 14}Reflect(stu2)fmt.Printf("stu2 = %v\n", stu2) // stu2 = {Bob 14}
}
程序的運行結果如下:
說明一下:
- 通過反射獲取變量對應的類型所綁定的方法數,以及獲取指定索引方法的Value封裝或方法信息時,如果變量的類型是
type
,則只能訪問到receiver為type
的方法,如果變量的類型是*type
,則能同時訪問到receiver為type
和*type
的方法。 - 因為receiver為
*type
的方法中可能會對變量的值進行修改,為了讓反射內部能夠找到需要被修改的變量并對其進行修改,這就要求變量的類型必須是*type
,因此如果變量的類型是type
,那就無法訪問到receiver為*type
的方法。 - Value的Call方法接收一個類型為
[]Value
的參數,表示在調用Value所持有的函數或方法時,需要傳入的各個參數的Value封裝,如果被調用的函數或方法無需傳入任何參數,則調用Call方法時傳入nil即可。同時Call方法會返回一個[]Value
類型的返回值,表示調用Value所持有的函數或方法得到的各個返回值的Value封裝。
實現函數適配器
實現函數適配器
通過反射可以實現函數適配器,具體步驟如下:
- 通過reflect.ValueOf函數,獲取函數的Value。
- 對用戶傳入的用于調用函數的參數進行Value封裝,并放到Value切片中。
- 通過Value的Call方法,指定參數調用所持有的函數,并返回函數調用的返回值。
案例如下:
package mainimport ("errors""fmt""reflect"
)func AddTwo(num1 int, num2 int) int {return num1 + num2
}func AddThree(num1 int, num2 int, num3 int) int {return num1 + num2 + num3
}func Bridge(f interface{}, args ...interface{}) (ret int, err error) {rVal := reflect.ValueOf(f)rKind := rVal.Kind()if rKind != reflect.Func {err = errors.New("the first arg is not a function")return}// 對傳入的參數進行Value封裝,并放到Value切片中num := len(args)argVals := make([]reflect.Value, num)for i := 0; i < num; i++ {argVals[i] = reflect.ValueOf(args[i])}retVals := rVal.Call(argVals) // 調用函數ret = int(retVals[0].Int())return
}func main() {ret, err := Bridge(AddTwo, 10, 20)if err != nil {fmt.Printf("err = %v\n", err)} else {fmt.Printf("ret = %d\n", ret) // ret = 30}ret, err = Bridge(AddThree, 10, 20, 30)if err != nil {fmt.Printf("err = %v\n", err)} else {fmt.Printf("ret = %d\n", ret) // ret = 60}
}
創建任意類型的變量
創建任意類型變量
通過反射可以創建任意類型的變量,具體步驟如下:
- 通過reflect.ValueOf和reflect.TypeOf函數,分別獲取二級指針的Value和Type,并繼續通過Value和Type的Elem方法,分別獲取二級指針指向的一級指針的Value和Type。
- 再次通過Type的Elem方法,繼續獲取一級指針指向的元素的Type,即需要創建的變量的類型。
- 通過reflect.New函數,創建指定Type的變量,并獲取持有指向該變量的指針的Value封裝elemVal。
- 通過Value的Set方法,將一級指針所持有的值設置為elemVal所持有的值,讓一級指針指向創建的變量。
案例如下:
package mainimport ("fmt""reflect"
)type Student struct {Name stringAge int
}func CreateObj(iVal interface{}) {rType := reflect.TypeOf(iVal).Elem() // 獲取二級指針指向的一級指針的TyperVal := reflect.ValueOf(iVal).Elem() // 獲取二級指針指向的一級指針的ValuerKind := rType.Kind()if rKind != reflect.Ptr { // 確保傳入的是二級指針(該類型指向的是一個指針類型)return}elemType := rType.Elem() // 獲取一級指針指向的元素的TypeelemVal := reflect.New(elemType) // 創建elemType類型的變量,并獲取持有指向該變量的指針的ValuerVal.Set(elemVal) // 將一級指針所持有的值設置為elemVal所持有的值
}func main() {var p1 *Studentfmt.Printf("p1 = %v\n", p1) // p1 = <nil>CreateObj(&p1)fmt.Printf("p1 = %v\n", p1) // p1 = &{ 0}var p2 *intfmt.Printf("p2 = %v\n", p2) // p2 = <nil>CreateObj(&p2)fmt.Printf("p2 = %v\n", p2) // p2 = 0xc00000e0f8
}
說明一下:
- 在創建變量時需要提供一個
*type
類型的指針,然后根據指針的類型創建一個type
類型的變量,并讓該指針指向這個變量,完成變量的創建。由于最終需要修改所給指針變量的指向,因此在調用CreateObj函數時需要傳入該指針的地址(二級指針)。