golang的反射與實踐(下)

上篇說了下反射該怎麼用,如今咱們來看一看使用反射的實際狀況,深刻理解一下golang

這篇由於是實踐篇,因此有大量的代碼示例來進行演示,由於只是演示反射的使用,因此對一些複雜的錯誤機制沒作處理json

反射自己並不難,看懂了上一章反射究竟是幹嗎用的,何時用,這一章其實很是好懂設計模式

說到底就是將reflect包提供給咱們的方法,進行一些組合使用罷了,說土一點就是調用下API數組

沒看上篇的能夠先看看golang的反射與實踐(上)微信

反射的實踐操做

好了,我們開始進行實踐app

先把咱們的準備工做作好,先定義一個Struct,再給這個結構體加上一些方法函數

// Employee 員工
type Employee struct {
	Name string `json:"emp_name"`
	Age  int    `json:"emp_age"`
	Sex  int
}

// GetSum 返回兩數之和
func (e *Employee) GetSum(n1, n2 int) int {
	return n1 + n2
}

// Set 接受值,給結構體e賦值
func (e *Employee) Set(name string, age, sex int) {
	e.Name = name
	e.Age = age
	e.Sex = sex
}

// Print 打印結構體*Employee 
func (e *Employee) Print() {
	log.Print("======Start======")
	log.Print(e)
	log.Print("======End======")
}
複製代碼

隨便給這個結構體寫了幾個方法,咱們主要是看,咱們如何使用反射在運行時對變量進行一個操做性能

使用反射來遍歷結構體的字段值,並獲取結構體的tag標籤

先來看個常規用法學習

// GetStruct 獲取結構體的字段及tag
func GetStruct(i interface{}) {
	rType := reflect.TypeOf(i)
	rVal := reflect.ValueOf(i)

	kd := rVal.Kind()

	// 若是是傳進來的是指針類型
	// 則獲取指針值
	if kd == reflect.Ptr {
		rType = rType.Elem()
		rVal = rVal.Elem()
		kd = rVal.Kind()
	}

	if kd != reflect.Struct {
		log.Panicf("Kind is %v not struct ", kd)
	}
	// 獲取結構體的字段數
	sNum := rVal.NumField()
	log.Printf("Struct has %v fields ", sNum)
	// 遍歷結構體的全部字段
	for i := 0; i < sNum; i++ {
		log.Printf("Field %d value is %v", i, rVal.Field(i))
		// 獲取Struct的tag,使用Type類型獲取
		tag := rType.Field(i).Tag.Get("json")
		if tag == "" {
			log.Printf("Field %d hasn't tag %v ", i, tag)
			continue
		}
		log.Printf("Field %d tag is %v ", i, tag)
	}
}
複製代碼

咱們定義一個方法GetStruct(i interface{}),由於入參是interface{}類型,因此這個方法能夠接收並處理全部的數據類型。這就是反射的牛逼之處了測試

遺憾的是,反射的性能比較低。後面我們對性能進行分析時再拿出來聊聊

測試用例以下

func TestGetStruct(t *testing.T) {
	emp := &Employee{}
	emp.Set("鬧鬧", 99, 0)
	GetStruct(emp)
}
複製代碼

執行結果以下圖所示

這個函數接受的參數是interface,也就是說,經過這個函數,無論入參傳遞了什麼樣的結構體,咱們能夠知道這個結構體有什麼標籤,有幾個方法

獲取tag標籤的用處就是對咱們的結構體進行序列化時使用,將結構體的字段名變成咱們須要的別名

想深刻了解的童鞋,能夠參考下encoding/json包的使用方式

獲取並調用結構體的方法

// CallMethod 調用結構體方法
// i : 傳入的struct
// methodByName : 調用結構體的方法名
func CallMethod(i interface{}, methodByName string) {
	rVal := reflect.ValueOf(i)
	rType := reflect.TypeOf(i)
	log.Printf("Type is %v Kind is %v", rType, rType.Kind())

	// 獲取結構體有多少個方法
	numOfMethod := rVal.NumMethod()
	log.Printf("Struct has %d method", numOfMethod)
	// 聲明Value數組
	var params []reflect.Value
	// 聲明一個Value類型,用於接收方法
	var method reflect.Value

	if methodByName == "GetSum" {
		// 調用方法時的參數
		params = append(params, reflect.ValueOf(10))
		params = append(params, reflect.ValueOf(88))

	}
	if methodByName == "Set" {
		// 調用方法時的參數
		params = append(params, reflect.ValueOf("鬧鬧吃魚"))
		params = append(params, reflect.ValueOf(18))
		params = append(params, reflect.ValueOf(0))
	}
	// 獲取方法
	method = rVal.MethodByName(methodByName)
	if !method.IsValid() {
		// 若是結構體不存在此方法,輸出Panic
		log.Panic("Method is invalid")
	}
	result := method.Call(params)
	if len(result) > 0 {
		// 若是函數存在返回值,則打印第一條
		log.Println("Call result is ", result[0])
	}
}
複製代碼

這裏值得注意一點的就是,咱們經過反射的Call去調用函數,傳入的參數的類型是reflect.Value類型,並非咱們定義函數時的int類型

因此在調用函數時傳入的參數須要進行一個類型轉換

給大家附上測試用例,大家能夠本身調試跑跑,會發現,無論你傳的結構體的字段是什麼,我都進行統一處理了

func TestCallMethod(t *testing.T) {
	emp := &Employee{}
	emp.Set("鬧鬧", 99, 0)
	emp.Print()
	CallMethod(emp, "Set")
	emp.Print()
}
複製代碼

修改字段值

// ModifyField 修改字段值
func ModifyField(i interface{}, filedName string) {
	rVal := reflect.ValueOf(i)
	filed := rVal.Elem().FieldByName(filedName)
	if !filed.IsValid() {
		log.Panic("filedName is invalid")
	}
	filed.SetString("鬧鬧")
}
複製代碼

運行時修改結構體的字段,主要就是作到一個通用性,好比上述的例子,不論是什麼結構體

依然附上測試用例

func TestModifyField(t *testing.T) {
	emp := &Employee{}
	ModifyField(emp, "Name")
}
複製代碼

無論傳入的結構體是什麼,只要包含了filedName(咱們指定的字段名),咱們就能夠對其進行值的更改

假如咱們有100個結構體,須要對name字段進行修改,經過反射的機制,咱們代碼的耦合度將大大的下降

定義適配器,用做統一處理接口

// Bridge 適配器
// 能夠實現調用任意函數
func Bridge(call interface{}, args ...interface{}) {
	var (
		function reflect.Value
		inValue  []reflect.Value
	)
	n := len(args)
	// 將參數轉換爲Value類型
	inValue = make([]reflect.Value, n)
	for i := 0; i < n; i++ {
		inValue[i] = reflect.ValueOf(args[i])
	}
	// 得到函數的Value類型
	function = reflect.ValueOf(call)
	// 傳參,調用函數
	function.Call(inValue)
}
複製代碼

寫了個測試用例,函數是咱們在調用Bridge前就已經定義好了

func TestBridge(t *testing.T) {
	call1 := func(v1, v2 int) {
		log.Println(v1, v2)
	}
	call2 := func(v1, v2 int, str string) {
		log.Println(v1, v2, str)
	}

	Bridge(call1, 1, 2)
	Bridge(call2, 2, 3, "callTest")
}

複製代碼

兩個函數是不一樣的函數,可是均可以經過Bridge進行執行

適配器有什麼用呢?若是不知道的童鞋,能夠去看看設計模式「適配器模式」

由於本篇幅只是說如何在實戰中應用反射,因此這裏就不講解設計模式了

使用反射建立,並操做結構體

// CreateStruct 使用反射建立結構體
// 並給結構體賦值
func CreateStruct(i interface{}) *Employee {
	var (
		structType  reflect.Type
		structValue reflect.Value
	)
	// 獲取傳入結構體指向的Type類型
	structType = reflect.TypeOf(i).Elem()
	// 建立一個結構體
	// structValue持有一個指向類型爲Type的新申請的指針
	structValue = reflect.New(structType)
	// 轉換成咱們要建立的結構體
	modle := structValue.Interface().(*Employee)
	// 取得structValue指向的值
	structValue = structValue.Elem()
	// 給結構體賦值
	structValue.FieldByName("Name").SetString("鬧鬧吃魚")
	structValue.FieldByName("Age").SetInt(100)
	return modle
}
複製代碼

使用方式就看看測試用例

func TestCreateStruct(t *testing.T) {
	emp := &Employee{
		Name: "NaoNao",
		Age:  18,
		Sex:  1,
	}
	emp.Print()// &{NaoNao 18 1}
	newEmp := CreateStruct(emp)
	newEmp.Print()// &{鬧鬧吃魚 100 0}
}
複製代碼

可能你會問,CreateStruct的入參不是interface{}嗎?可爲何我傳一個任意的結構體,卻要給返回一個指定的結構體呢?就不能我傳什麼結構體進去,就返回什麼結構體出來嗎?

理想老是豐滿的,現實倒是很是骨感,雖然咱們反射的方法實現,都是將入參寫爲interface{},但使用反射並非意味着咱們必定就寫了一個萬能的程序

還記得上一篇提到的,變量reflect.Value之間該如何轉換嗎?

我們再複習一下:變量<------>interface{}<------>reflect.Value

咱們不論是把Value轉爲結構體,仍是轉爲基本類型,咱們都須要在編譯前肯定轉換後的類型

換句話說,只要咱們在運行時牽扯到類型的轉換,咱們都須要各類if來判斷是否能轉換成咱們須要的類型

本文以大量的代碼實現來闡述反射該怎麼用,說實話,挺無聊的

寫這篇文章的目的就是讓你拿電腦上去編譯跑跑,或者何時想到要用反射了,能夠拿出來瞅瞅,看看什麼地方須要用到反射,反射又能夠幹什麼

image

微信掃碼關注公衆號「鬧鬧吃魚」,還可領取Go語言學習大禮包,入門到進階再也不了無頭緒

相關文章
相關標籤/搜索