Go 中的反射 reflect 介紹和基本使用

一、什麼是反射

在計算機科學中,反射(英語:reflection)是指計算機程序在運行時(runtime)可以訪問、檢測和修改它本身狀態或行爲的一種能力。用比喻來說,反射就是程序在運行的時候能夠“觀察”並且修改自己的行爲。(來自wikipedia)

反射是程序審查自身結構的能力,並能對程序做出一定的修改。

對於人來說,審查自身或過往事情的能力,叫 “反思” 或 "反省"。

二、Go 中的反射包:reflect介紹

Go 中的反射是用 reflect 包實現,reflect 包實現了運行時的反射能力,能夠讓程序操作不同的對象。

Go 中的反射是建立在類型系統之上,它與空接口 interface{} 密切相關。

每個 interface{} 類型的變量包含一對值 (type,value),type 表示變量的類型信息,value 表示變量的值信息。

所以 nil!=nil,就可以理解了。

空接口 interface{} 源碼簡析:https://www.cnblogs.com/jiujuan/p/12653806.html

  • 獲取 2 種類型信息的方法:

reflect.TypeOf() 獲取類型信息,返回 Type 類型;

reflect.ValueOf() 獲取數據信息,返回 Value 類型。

image-20230222025904009

  • 2 個方法部分源碼:
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
// ValueOf用來獲取輸入參數接口中的數據的值,如果接口爲空則返回0
// 參數類型 interface{} 可以表示任意類型
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
// 參數類型 interface{} 可以表示任意類型
func TypeOf(i interface{}) Type {...}

通過 reflect.TypeOf() 和 reflect.ValueOf() ,經過中間變量 interface{},把一個普通的變量轉換爲反射包中類型對象: Type 和 Value 2 個類型,然後再用 reflect 包中的方法對它們進行各種操作。

步驟:Go 變量 -> interface{} -> 反射包的反射類型對象

image-20230221155934865

反射包 reflect 中所有方法基本都是圍繞 Type 和 Value 這 2 個類型設計和操作。

https://pkg.go.dev 上可以查看有關 Type 和 Value 的所有方法,以及其它類型方法:

image-20230220180637118image-20230220180651792

三、reflect簡單使用

從上面可以看出 TypeOf() 返回的是一個反射包中的 Type 類型,ValueOf() 返回的是一個反射包中的 Value 類型。

例 1:float 反射示例

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 1.2345

	fmt.Println("==TypeOf==")
	t := reflect.TypeOf(x)
	fmt.Println("type: ", t)
	fmt.Println("kind:", t.Kind())

	fmt.Println("==ValueOf==")
	v := reflect.ValueOf(x)
	fmt.Println("value: ", v)
	fmt.Println("type:", v.Type())
	fmt.Println("kind:", v.Kind())
	fmt.Println("value:", v.Float())
	fmt.Println(v.Interface())
	fmt.Printf("value is %5.2e\n", v.Interface())

	y := v.Interface().(float64)
	fmt.Println(y)
    
    fmt.Println("===kind===")
	type MyInt int
	var m MyInt = 5
	v = reflect.ValueOf(m)
    fmt.Println("kind:", v.Kind()) // Kind() 返回底層的類型 int
    fmt.Println("type:", v.Type()) // Type() 返回類型 MyInt
}

運行輸出:

go run ch1.go

TypeOf
type: float64
kind: float64
ValueOf
value: 1.2345
type: float64
kind: float64
value: 1.2345
1.2345
value is 1.23e+00
1.2345
=kind=
kind: int
type: main.MyInt

上面的例子,reflect 包中 reflect.TypeOf() 返回 Type 和 reflect.ValueOf() 返回 Value 類型 都有一個 Kind() 方法,Kind() 返回一個底層的數據類型,如 Unit,Float64,Slice, Int 等。

reflect.ValueOf() 返回的 Value 類型

  • 它有一個 Type() 方法,返回的是 reflect.Value 的 Type
  • 它有獲取 Value 類型值的方法
    • 如果我們知道是 float 類型,所以直接用 Float() 方法。
    • 如果不知道具體類型呢?由上面例子可知用 Interface() 方法,然後在進行類型斷言 v.Interface().(float64) 來判斷獲取值

v.Kind() 和 v.Type() 區別

上例中,在 type MyInt int 裏,v.Kind()v.Type() 返回了不同的類型值,Kind() 返回的是 intType() 返回的是 MyInt
在 Go 中,可以用 type 關鍵字定義自定義類型,Kind() 方法返回底層類型。比如還有結構體,指針等類型用 type 定義的,那麼 Kind() 方法就可以獲取這些類型的底層類型。

例 2:struct 反射示例

package main

import (
	"fmt"
	"reflect"
)

type student struct {
	Name string `json:"name"`
	Age  int    `json:"age" id:"1"`
}

func main() {
	stu := student{
		Name: "hangmeimei",
		Age:  15,
	}

	valueOfStu := reflect.ValueOf(stu)
	// 獲取struct字段數量
	fmt.Println("NumFields: ", valueOfStu.NumField())
	// 獲取字段 Name 的值
	fmt.Println("Name value: ", valueOfStu.Field(0).String(), ", ", valueOfStu.FieldByName("Name").String())
	// 字段類型
	fmt.Println("Name type: ", valueOfStu.Field(0).Type())

	typeOfStu := reflect.TypeOf(stu)
	for i := 0; i < typeOfStu.NumField(); i++ {
		// 獲取字段名
		name := typeOfStu.Field(i).Name
		fmt.Println("Field Name: ", name)

		// 獲取tag
		if fieldName, ok := typeOfStu.FieldByName(name); ok {
			tag := fieldName.Tag

			fmt.Println("tag-", tag, ", ", "json:", tag.Get("json"), ", id", tag.Get("id"))
		}
	}
}

輸出:

$ go run .\get_struct_val_simple.go
NumFields:  2
Name value:  hangmeimei ,  hangmeimei
Name type:  string
Field Name:  Name
tag- json:"name" ,  json: name , id
Field Name:  Age
tag- json:"age" id:"1" ,  json: age , id 1

獲取 struct 信息的一些方法:

  • NumField() 獲取結構體字段數量
  • Field(i) 可以通過 i 字段索引來獲取結構體字段信息,比如 Field(i).Name 獲取字段名
  • FieldByName(name) 通過 name 獲取字段信息

四、反射三定律

在 Go 官方博客文章 laws-of-reflection 中,敘述了反射的 3 定律:

  • 第一定律:反射是從接口值到反射對象
  • 第二定律:從反射對象可以獲取接口值
  • 第三定律:要修改反射對象的值,其值必須可以設置

第一定律

在一般情況下,反射是一種檢查存儲在接口變量中的類型和值的機制。

這其實從 reflect 包中的 TypeOf 和 ValueOf 函數就可以知道。在本文第二節中有講,這 2 個函數的接收參數就是 interface{}。

比如 reflect.TypeOf(6.4),調用 reflect.TypeOf(x) 時(這裏的 x 表示 6.4),x 首先存儲在一個空接口 interface{} 中,作爲參數傳遞,reflect.TypeOf 對該接口進行類型信息解碼,獲取類型詳細信息。

func TypeOf(i interface{}) Type {...}
func ValueOf(i interface{}) Value {...}

reflect.Type 和 reflect.Value 都有很多方法讓我們來操作他們。

重要的地方:

  • Value 有個返回類型的方法 Type()(見上面第三小節例1)
  • Value 和 Type 都有一個 Kind() 方法,它返回一個常量,如 Uint,Float64,Slice等等,表示底層數據類型

第二定律

給定一個 reflect.Value 我們可以使用 Interface 方法獲取接口值。

func (v Value) Interface() interface{}

比如:

y := v.Interface().(float64)

(見上面第三小節例1,知道數據類型直接獲取值方法,不知道數據類型用 Interface() 獲取數據然後斷言值)

用 Interface() 方法獲取值,還可以用 Printf 直接打印值,不用斷言。

說明:反射從接口值到反射對象,然後在返回接口的各種信息

第三定律

看一個例子:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

這個問題並不是 7.1 不可尋址,而是這個 x 不可設置。

可設置性是反射值的一個屬性,並不是所有的反射值有這個屬性。

Value 的 CanSet 方法可以獲取值是否可設置,如:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

output:

settability of v: false

爲什麼有可設置性?

因爲 reflect.ValueOf(x) 這個 x 傳遞的是一個原數據的副本,上面代碼 v.SetFloat(7.1) 如果設置成功,那麼更新的是副本值,原始值 x 並沒有更新。這就會造成原值和新值的混亂,可設置屬性就是避免這個問題。

那怎麼辦?

傳遞的是一個副本,而不是值本身。如果希望能直接修改 x,那麼必須把 x 的地址傳遞給函數,即指向 x 的指針:

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

output:

type of p: *float64
settability of p: false

還是 false,爲什麼?

反射對象 p 不可設置,它並不是我們要設置的 p,它實際上是 *p。爲了得到 p 所指向的東西,我們需要調用 Value 的 Elem 方法,通過指針進行簡介尋址,然後將結果保存在一個名爲 v 的反射 Value 中:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

現在 v 是一個可設置的反射對象,輸出:

settability of v: true

然後我們可以用 v.SetFloat() 設置值:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

output:

7.1
7.1

說明:請記住,修改反射值需要值的地址,以便修改他們的真正值。

以上來自 Go blog-The Laws of Reflection:https://go.dev/blog/laws-of-reflection

五、Kind() 和 Type() 方法區別

上面第三小節例1中有說明,這裏在作個小節。

如定義 type MyInt intv.Kind()v.Type() 返回了不同的類型值,Kind() 返回的是 intType() 返回的是 MyInt
在 Go 中,可以用 type 關鍵字定義自定義類型,Kind() 方法返回底層數據類型,比如這裏的 int

比如還有結構體,指針等類型用 type 定義,那麼 Kind() 方法就可以獲取這些類型的底層類型。

在源碼中 Kind() 方法,返回一個常量,如 Uint,Float64,Slice 等等,表示底層數據類型。

六、反射設置變量值

可設置說明

在上面第四小節第三定律中有講到反射中值的可設置性,詳細情況去看第三定律這一段。

可以用 CanSet 方法來判斷值是否可以設置。

在 Go 中,函數參數的傳遞都是值拷貝,在反射中要修改值,必須傳遞指針,並且用 Elem() 方法獲取指針的值,然後進行修改。

例 3:設置變量值

package main

import (
	"fmt"
	"reflect"
)

type student struct {
	Age int
}

func main() {
	var stu student
	fmt.Println("origin age: ", stu.Age)

	valOfStu := reflect.ValueOf(&stu)

	canSet := valOfStu.CanSet()
	fmt.Println("can set: ", canSet)

	valOfStu = valOfStu.Elem()

	field := valOfStu.FieldByName("Age")

	field.SetInt(23)
	fmt.Println("set age: ", field.Int())

	//====================
	fmt.Println("====================")
	var num int32 = 24
	v := reflect.ValueOf(&num)
	fmt.Println("num:", num, ", elem Kind: ", v.Elem().Kind())
	if v.Elem().Kind() == reflect.Int32 {
		v.Elem().SetInt(300)
	}
	fmt.Println("set num: ", num)
}

輸出:

origin age:  0
can set:  false
set age:  23
====================
num: 24 , elem Kind:  int32
set num:  300

七、反射調用方法

例 4:反射調用方法

package main

import (
	"fmt"
	"reflect"
)

type student struct {
	Name  string
	Age   int
	Name2 string
}

func (stu student) SetName(name string, name2 string) {
	stu.Name = name
	stu.Name2 = name2
}

func (stu student) SetAge(age int) {
	stu.Age = age
}

func (stu student) Print() string {
	return fmt.Sprintf("Name: %s, Age: %d, Name2: %s", stu.Name, stu.Age, stu.Name2)
}

func main() {
	stu := student{"tom", 23, "HanMei"}
	fmt.Println("orgin student: ", stu)

	fun := reflect.ValueOf(&stu).Elem()
	fmt.Println(fun.MethodByName("Print").Call(nil)[0])

	params := make([]reflect.Value, 2)
	params[0] = reflect.ValueOf("Tom")
	params[1] = reflect.ValueOf("LiLei")
	fun.MethodByName("SetName").Call(params)

	params2 := make([]reflect.Value, 1)
	params2[0] = reflect.ValueOf(34)
	fun.MethodByName("SetAge").Call(params2)

	fmt.Println(fun.MethodByName("Print").Call(nil))
}

輸出:

orgiin student:  {tom 23 HanMei}
Name: tom, Age: 23, Name2: HanMei
[Name: tom, Age: 23, Name2: HanMei]

用 reflect.ValueOf 調用 MethodByName() 方法,然後在調用 Call() 傳入參數。

例 5:MakeFunc 調用函數

官方的一個例子:https://pkg.go.dev/[email protected]#MakeFunc

package main

import (
	"fmt"
	"reflect"
)

// https://pkg.go.dev/[email protected]#MakeFunc
func main() {
	// swap is the implementation passed to MakeFunc.
	// It must work in terms of reflect.Values so that it is possible
	// to write code without knowing beforehand what the types
	// will be.
	swap := func(in []reflect.Value) []reflect.Value {
		return []reflect.Value{in[1], in[0]}
	}

	// makeSwap expects fptr to be a pointer to a nil function.
	// It sets that pointer to a new function created with MakeFunc.
	// When the function is invoked, reflect turns the arguments
	// into Values, calls swap, and then turns swap's result slice
	// into the values returned by the new function.
    makeSwap := func(fptr interface{}) {
		// fptr is a pointer to a function.
		// Obtain the function value itself (likely nil) as a reflect.Value
		// so that we can query its type and then set the value.
		fn := reflect.ValueOf(fptr).Elem()

		// Make a function of the right type.
		v := reflect.MakeFunc(fn.Type(), swap)

		// Assign it to the value fn represents.
		fn.Set(v)
	}

	// Make and call a swap function for ints.
	var intSwap func(int, int) (int, int)
	makeSwap(&intSwap)
	fmt.Println(intSwap(0, 1))

	// Make and call a swap function for float64s.
	var floatSwap func(float64, float64) (float64, float64)
	makeSwap(&floatSwap)
	fmt.Println(floatSwap(2.72, 3.14))
}

八、反射優缺點

優點

  • 可以根據條件靈活的調用函數。最大一個優點就是靈活。

比如函數參數的數據類型不確定,這時可以根據反射來判斷數據類型,在調用適當的函數。

還有比如根據某些條件來調用哪個函數。

需要根據動態需要來調用函數,可以用反射。

使用反射的 2 個典型場景:1、操作數據庫的 ORM 框架 ,2、依賴注入

缺點

  • 用反射編寫的代碼比較難以閱讀和理解

  • 反射是在運行時才執行,所以編譯期間比較難以發現錯誤

  • 反射對性能的影響,比一般正常運行代碼慢一到兩個數量級。

    這裏性能其實是你的業務量到了一定時候纔要注意。量不大情況,夠用

九、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章