Go語言備忘錄(2):反射的原理與使用詳解


本文內容是本人對Go語言的反射原理與使用的備忘錄,記錄了關鍵的相關知識點,以供翻查。 文中如有錯誤的地方請大家指出,以免誤導!轉摘本文也請注明出處:Go語言備忘錄(2):反射的原理與使用詳解,多謝! 參考書籍《The Go Programming Language》、《Go In Action》、《Go語言學習筆記》等

目錄:

  1. 預備知識
  2. reflect.Typeof、reflect.ValueOf
  3. Value、Type
  4. 動態調用
  5. 通過反射可以修改原對象
  6. 實現類似“泛型”的功能
 
1.預備知識:
  • Go的變量都是靜態類型(聲明時指定的類型),它也有底層類型(定義類型時指定的基礎類型,即:它是以什么形式存儲的);
  • 一個接口變量存儲了一對(value, type):賦值給這個接口變量的具體值value、以及這個值的類型描述符type;
  • Go的接口變量都是靜態類型化的:一個接口類型變量總是保持同一個靜態類型(即聲明時指定的接口類型),即使在運行時它保存的值的類型發生變化,這些值總是滿足這個接口。
  • 接口的靜態類型決定了能用接口變量調用哪些方法(接口中定義的方法,它們是保存的值的方法集的子集);
  • 反射是一種檢查存儲在接口變量中的(value, type)對的機制,反射操作所需的全部信息都源自接口變量(通過把變量轉換為空接口變量,從而獲得了該變量的value、type,這樣就可以進行一系列的“反射操作”);
  • reflect包中的兩個類型:Type和Value,這兩種類型提供了訪問一個接口變量中所包含的(value, type)對的途徑;

2.反射由reflect包提供支持,主要方法:

  • func TypeOf ( i interface{} ) Type:
    如 reflect.Typeof(x) ,形參x被保存為一個接口值並作為參數傳遞(復制),方法內部會把該接口值拆包恢復出x的類型信息保存為reflect.Type並返回;
  • func ValueOf ( i interface{} ) Value:
    如 reflect.ValueOf(x) ,形參被保存為一個接口值並作為參數傳遞(復制), 方法內部把該接口值的值恢復出來保存為reflect.Value並返回;
3.reflect包的兩個主要類型Value、Type :這兩種類型都提供了大量的方法讓我們可以檢查和操作這兩種類型
  • Type 接口:可以表示一個Go類型
    • Kind() 將返回一個常量,表示具體類型的底層類型
    • Elem()方法返回指針、數組、切片、map、通道的基類型;
    • 可用反射提取struct tag,還能自動分解,常用於ORM映射、數據驗證等;
    • 輔助判斷方法Implements()、ConvertibleTo()、AssignableTo()
  • Value 結構體:可以持有一個任意類型的值
    • 調用 Value 的 Type() 將返回具體類型所對應的 reflect.Type(靜態類型)
    • 調用 Value 的 Kind() 將返回一個常量,表示具體類型的底層類型
    • Interface方法是ValueOf方法的逆,它把一個reflect.Value恢復成一個接口值:把Value中保存的類型和值的信息打包成一個接口表示並返回;如:
      y,ok := v.Interface().(float64) // y 的類型被斷言為 float64
      fmt.Println(y)
      以上可簡寫為這樣:
      fmt.Println(v.Interface()) //fmt.Println會把它恢復出來
    • 通道類型的反射對象:有TrySend()、TryRecv()方法;
    • IsNil()方法判斷反射對象保存的值是否為nil;
4.通過反射可以動態調用原對象的導出方法:
 
v := reflect.ValueOf(&x)
m := v.MethodByName("Show")
in := []reflect.Value{
    reflect.ValueOf(23),
    reflect.ValueOf(323),
}
out := m.Call(in)  //對於變參可用CallSlice方法
 
5.通過反射可以修改原對象:
  • 原理:
    • 因為給Go的函數、方法傳遞的都是形參的副本,同樣的,反射一個對象時,形參被保存為一個接口對象並作為參數傳遞(復制),該接口變量是non-settable的,返回的Value也是non-settable的,對它調用Set方法會出現錯誤;
    • Value的CanSet方法用於測試一個Value的Settablity性質,它有點像unaddressability,但是更加嚴格,描述的是一個反射對象能夠修改創造它的那個實際存儲的值的能力。settability由反射對象是否保存原始項而決定。
    • 所以,如果想通過反射來修改對象,必須先把該對象的指針傳給reflect.ValueOf(&x),這樣得到的Value對象內部就保存了原對象指針的副本,只有找到該指針指向的值才能修改原始對象,通過Elem()方法就可以獲得一個保存了原對象的Value對象,此時的Value對象就是settable的;
對於一個settable的Value反射對象,如 d := reflect.ValueOf(&x).Elem():
  • d.CanAddr()方法:判斷它是否可被取地址
  • d.CanSet()方法:判斷它是否可被取地址並可被修改
通過一個settable的Value反射對象來訪問、修改其對應的變量的方式:
  • 方式1:通過把反射對象轉換回原對象類型的指針,然后直接修改該指針
    • px := d.Addr().Interface().(*int)
    • 第一步是調用Addr()方法,它返回一個Value,里面保存了指向變量的指針。
    • 然后是在Value上調用Interface()方法,也就是返回一個interface{},里面通用包含指向變量的指針。
    • 最后,如果我們知道變量的類型,我們可以使用類型的斷言機制將得到的interface{}類型的接口強制環為普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了
  • 方式2:可直接通過Set()方法來修改
    • d.Set(reflect.ValueOf(4))
    • SetInt、SetUint、SetString和SetFloat等方法:d.SetInt(3),注意:雖然如SetInt()等方法只要參數變量的底層數據類型是有符號整數就可以工作,但不能是一個引用interface{}類型的reflect.Value
  • 小結:Value反射對象為了修改它們所表示的東西必須要有這些東西的地址
  • 例子:
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意這里:把x地址傳進去了!
fmt.Println(p.Type())  //*float64
fmt.Println(p.CanSet())  //false 這里的p只是指針,仍然是non-settable的
v := p.Elem() //此時的v保存了x
fmt.Println( v.CanSet()) //true 
v.SetFloat(7.1)
fmt.Println(v.Interface()) //7.1
fmt.Println(x) //7.1

雖然反射可以越過Go語言的導出規則的限制讀取結構體中未導出的成員,但不能修改這些未導出的成員。因為一個struct中只有被導出的字段才是settable的。

type T struct {
    A int
    B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()//把s.Type()返回的Type對象復制給typeofT,typeofT也是一個反射。
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)//迭代s的各個域,注意每個域仍然是反射。
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())//提取了每個域的名字
}
//0: A int = 23
//1: B string = skidoo

s.Field(0).SetInt(77) //s.Field(0).Set(reflect.ValueOf(77))
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)  //t is now {77 Sunset Strip}
 
6.反射庫提供了內置函數make和new的對應操作,如reflect.MakeFunc()方法,通過它可以實現類似“泛型”的功能:
  • 定義一個可適應不同數據類型的通用模板算法函數,然后用reflect.MakeFunc()方法,可以把任意函數類型變量綁定到通用模板算法函數(為一系列函數對象指定同一個函數體);
package main

import (
	"reflect"
	"strings"
	"fmt"
)
//通用算法函數體模板
func add(args []reflect.Value) (results []reflect.Value) {
	if len(args) == 0 {
		return nil
	}
	var r reflect.Value
	switch args[0].Kind() {
	case reflect.Int:
		n:=0
		for _,a:=range args{
			n+=int(a.Int())
		}
		r = reflect.ValueOf(n)
	case reflect.String:
		ss := make([]string,0,len(args))
		for _,s:=range args{
			ss = append(ss,s.String())
		}
		r=reflect.ValueOf(strings.Join(ss,""))
	}
	results = append(results,r)
	return
}
func makeAdd(T interface{})  {
	fn:=reflect.ValueOf(T).Elem()
	v:=reflect.MakeFunc(fn.Type(),add) //把原始函數變量的類型和通用算法函數存到同一個Value中
	fn.Set(v)  //把原始函數指針變量指向v,這樣它就獲得了函數體
}
func main() {
    //定義函數變量,未定義函數體
	var intAdd func(x,y int) int
	var strAdd func(a,b string) string

	makeAdd(&intAdd)
	makeAdd(&strAdd)

	fmt.Println(intAdd(12,23))  //35
	fmt.Println(strAdd("hello, ","world!")) //hello, world!
}
 
 最后,反射對性能有一定的影響,如對性能要求較高,須謹慎使用反射!
 


免責聲明!

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



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