golang中的反射reflect詳解


先重復一遍反射三定律:

    1.反射可以將“接口類型變量”轉換為“反射類型對象”。

    2.反射可以將“反射類型對象”轉換為“接口類型變量”。

    3.如果要修改“反射類型對象”,其值必須是“可寫的”(settable)

總結

下面詳細說明了Golang的反射reflect的各種功能和用法,都附帶有相應的示例,相信能夠在工程應用中進行相應實踐,總結一下就是:

  • 反射可以大大提高程序的靈活性,使得interface{}有更大的發揮余地

    • 反射必須結合interface才玩得轉
    • 變量的type要是concrete type的(也就是interface變量)才有反射一說
  • 反射可以將“接口類型變量”轉換為“反射類型對象”

    • 反射使用 TypeOf 和 ValueOf 函數從接口中獲取目標對象信息
  • 反射可以將“反射類型對象”轉換為“接口類型變量

    • reflect.value.Interface().(已知的類型)
    • 遍歷reflect.Type的Field獲取其Field
  • 反射可以修改反射類型對象,但是其值必須是“addressable”

    • 想要利用反射修改對象狀態,前提是 interface.data 是 settable,即 pointer-interface
  • 通過反射可以“動態”調用方法

  • 因為Golang本身不支持模板,因此在以往需要使用模板的場景下往往就需要使用反射(reflect)來實現

package main

import (
	"fmt"
	"io"
	"math"
	"os"
	"reflect"
)

func main() {
	// 反射的機制就是在運行時動態的調用對象的方法和屬性
	// 官方自帶的reflect就是反射相關的, golang中的grpc就是通過反射實現的

	// 變量包括(type,value)兩部分
	// type又分為:static type,concrete type
	//簡單來說static type是編碼時看見的類型,例如:int string
	// concrete type 是runtime系統看見的類型
	// 類型斷言是否能成功取決於concrete type,而不是static type
	// 因此一個reader變量如果他的 concrete type 也實現了write方法的話,它也可以被類型斷言為writer

	// 反射主要與golang的interface類型相關(它的type是concrete type),只有interface類型才有反射一說
	// 在go中每個interface變量都有一個對應的pair,pair中記錄了實際變量的值和類型(value type)
	// value記錄了實際變量的值,type記錄了實際變量的類型
	// 一個interface類型的變量包含了兩個指針,一個指針指向了值得類型(concrete type),一個指針指向了實際的值(value)

	// 1. interface和反射

	// tty pair<type:*os.File, value:&os.File{}>
	tty, _ := os.OpenFile("./test.txt", os.O_RDWR, 0)

	var r io.Reader
	// r pair<type:*os.File, value:&os.File{}>
	// 這個pair在接口變量的連續賦值過程中是不變的
	r = tty

	var w io.Writer
	// w pair<type:*os.File, value:&os.File{}>
	// 將這個接口變量r賦值給另外一個接口變量w
	w = r.(io.Writer)

	// 接口變量w的pair與r的pair相同,即使w是空接口,pair也是不變的
	fmt.Println(w)

	// interface以及pair的存在,是golang中實現反射的前提,理解了pair就更容易理解反射
	// 反射就是用來檢測存儲在接口變量內部(值value,實際類型concrete type) pair對的一種機制

	// 2. golang的反射reflect
	// reflect的基本功能 TypeOf ValueOf
	fmt.Println(reflect.TypeOf(15), reflect.ValueOf(15).Interface().(int))

	// reflect.TypeOf是獲取pair中的type,reflect.ValueOf是獲取pair中的value
	// 也就是說反射可以將 接口類型的變量 轉換為 反射類型對象,反射值得是reflect.Type,reflect.Value這兩種

	// 3. 從reflect.Value中獲取接口interface的信息
	// 當執行reflect.ValueOf(interface)之后,就得到了一個類型為reflect.Value變量,可以通過它本身的Interface()方法
	// 獲取接口變量的真是內容,然后可以根據類型判斷進行轉換,轉換為原有真實類型,我們可以已知原有類型,也有可能未知原有類型
	// (1) 已知原有類型,進行強制轉換
	// realValue := value.Interface().(已知的類型)
	// 示例如下
	var f1 float32 = 3.14567
	pointer := reflect.ValueOf(&f1)
	value := reflect.ValueOf(f1) // 反射將接口類型的變量轉換為反射類型的對象  reflect.Value類型

	// 可以理解為強制轉換,需要注意的是, 轉換的時候,如果轉換的類型不完全符合,直接panic
	// go對類型要求非常嚴格,一定要完全符合
	// 如下兩個,一個是*float32, 一個是float32,如果弄混直接報錯
	// value.Interface()  將反射類型的對象轉換為接口類型的變量
	// value.Interface().(float32)  斷言接口類型變量的具體類型
	convertPointer := pointer.Interface().(*float32)
	convertValue := value.Interface().(float32)
	fmt.Println(convertPointer, convertValue)

	// 說明:轉換的時候,如果轉換的類型不完全符合,則直接panic,類型要求非常嚴格
	// 轉換的時候要區分指針還是值
	// 也就是說反射可以將反射類型的對象轉換為接口類型的變量

	// (2) 未知原有類型,【遍歷探測其Filed】
	// 案例
	var user = User{1, "lisi", 18}
	DoFieldAndMethod(user)

	// 4. reflect.Value 設置實際變量的值
	var f2 = math.Pi
	f2Value := reflect.ValueOf(&f2)  // reflect.Value對象類型
	// reflect.Value.Elem() 獲取原始值對應的反射對象,只有原始對象才能修改,當前反射對象是不能修改的
	newValue := f2Value.Elem()  // reflect.Value對象類型
	fmt.Println(newValue.Type(), newValue.CanSet())
	newValue.SetFloat(77)
	fmt.Println(f2)
	/*
	說明
	需要傳入的參數是* float64這個指針,然后可以通過pointer.Elem()去獲取所指向的Value,注意一定要是指針。
	如果傳入的參數不是指針,而是變量,那么
	通過Elem獲取原始值對應的對象則直接panic
	通過CanSet方法查詢是否可以設置返回false
	newValue.CantSet()表示是否可以重新設置其值,如果輸出的是true則可修改,否則不能修改,修改完之后再進行打印發現真的已經修改了。
	reflect.Value.Elem() 表示獲取原始值對應的反射對象,只有原始對象才能修改,當前反射對象是不能修改的
	也就是說如果要修改反射類型對象,其值必須是“addressable”【對應的要傳入的是指針,同時要通過Elem方法獲取原始值對應的反射對象】
	struct 或者 struct 的嵌套都是一樣的判斷處理方式
	 */

	// 5. 通過reflect.ValueOf來進行方法的調用
	// (1). 要通過反射來調用起對應的方法,必須要先通過reflect.ValueOf(interface)來獲取到reflect.Value,得到“反射類型對象”后才能做下一步處理
	userValue := reflect.ValueOf(user)
	// 一定要指定參數為正確的方法名
	// (2). 先看看帶有參數的調用方法
	methodValue := userValue.MethodByName("ReflectCallFunc")
	args := []reflect.Value{reflect.ValueOf(11), reflect.ValueOf("wupeiqi")}
	methodValue.Call(args)
	// (3)在看看不帶參數的調用方法
	methodValue = userValue.MethodByName("Call")
	args = make([]reflect.Value, 0)
	methodValue.Call(args)
	/*
	說明
	要通過反射來調用起對應的方法,必須要先通過reflect.ValueOf(interface)來獲取到reflect.Value,得到“反射類型對象”后才能做下一步處理
	reflect.Value.MethodByName這.MethodByName,需要指定准確真實的方法名字,如果錯誤將直接panic,MethodByName返回一個函數值對應的reflect.Value方法的名字。
	[]reflect.Value,這個是最終需要調用的方法的參數,可以沒有或者一個或者多個,根據實際參數來定。
	reflect.Value的 Call 這個方法,這個方法將最終調用真實的方法,參數務必保持一致,如果reflect.Value'Kind不是一個方法,那么將直接panic。
	本來可以用u.ReflectCallFuncXXX直接調用的,但是如果要通過反射,那么首先要將方法注冊,也就是MethodByName,然后通過反射調用methodValue.Call
	 */

	// 6. Golang的反射reflect性能
	//Golang的反射很慢,這個和它的API設計有關。在 java 里面,我們一般使用反射都是這樣來弄的。
	type_ := reflect.TypeOf(user)
	field, _ := type_.FieldByName("Name")
	//這里取出來的 field 對象是 reflect.StructField 類型,但是它沒有辦法用來取得對應對象上的值。
	//如果要取值,得用另外一套對object,而不是type的反射
	fmt.Println(field.Name)  // 打印的是Name

	value_ := reflect.ValueOf(user)
	fieldValue := value_.FieldByName("Name")
	// 這里取出來的 fieldValue 類型是 reflect.Value,它是一個具體的值,而不是一個可復用的反射對象了
	// 每次反射都需要malloc這個reflect.Value結構體,並且還涉及到GC。
	fmt.Println(fieldValue.Interface().(string))
	/* 小結:
	Golang reflect慢主要有兩個原因
	涉及到內存分配以及后續的GC;
	reflect實現里面有大量的枚舉,也就是for循環,比如類型之類的。
	 */

}

type User struct {
	Id   int
	Name string
	Age  int
}

func (u User) ReflectCallFunc(a int, b string) {
	fmt.Println("reflectCallFunc is called!!!")
}
func (u User) Call() {
	fmt.Println("call")
}

func DoFieldAndMethod(user interface{}) {
	userType := reflect.TypeOf(user)
	userValue := reflect.ValueOf(user)
	// 注意:方法名和字段名首字母一定要大寫,否則獲取不到
	// 遍歷獲取字段名、類型、值
	for i := 0; i < userType.NumField(); i++ {
		field := userType.Field(i)
		value := userValue.Field(i).Interface()
		fmt.Println(field.Name, field.Type, value)
	}
	// 遍歷獲取方法名和方法類型
	for i := 0; i < userType.NumMethod(); i++ {
		m := userType.Method(i)
		fmt.Println(m.Name, m.Type)
	}

	/*
		說明
		通過運行結果可以得知獲取未知類型的interface的具體變量及其類型的步驟為:

		先獲取interface的reflect.Type,然后通過NumField進行遍歷
		再通過reflect.Type的Field獲取其Field
		最后通過Field的Interface()得到對應的value
		通過運行結果可以得知獲取未知類型的interface的所屬方法(函數)的步驟為:

		先獲取interface的reflect.Type,然后通過NumMethod進行遍歷
		再分別通過reflect.Type的Method獲取對應的真實的方法(函數)
		最后對結果取其Name和Type得知具體的方法名
		也就是說反射可以將“反射類型對象”再重新轉換為“接口類型變量”
		struct 或者 struct 的嵌套都是一樣的判斷處理方式
	*/

}

  

參考鏈接:https://studygolang.com/articles/12348?fr=sidebar

 

反射中type和kind的區別

 


免責聲明!

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



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