golang反射機制介紹
Go語言提供了一種機制贺辰,在編譯時不知道類型的情況下婆咸,可更新變量、在運行時查看值台舱、調用方法以及直接對他們的布局進行操作,這種機制稱為反射(reflection)潭流。
reflect.Type 和 reflect.Value
反射功能由reflect包提供竞惋,它定義了兩個重要的類型:Type 和 Value,分別表示變量的類型和值灰嫉。反射包里面拆宛,提供了reflect.TypeOf 和 reflect.ValueOf,返回被檢查對象的Type 和 Value:
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
可以看到讼撒,TypeOf和ValueOf的參數(shù)是interface{}類型浑厚,當把一個具體值賦給一個interface{}類型時,會發(fā)生一個隱式的類型轉換根盒,轉換會生成一個包含兩個部分內(nèi)容的接口值:動態(tài)類型部分是操作數(shù)的類型钳幅,動態(tài)值部分是操作數(shù)的值。
reflect.TypeOf返回接口值對應的動態(tài)類型炎滞,注意返回的時具體值類型而不是接口類型敢艰。比如下面的輸出的是"*os.File"而不是"io.Writer":
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(v)) // "*os.File"
同理,reflect.ValueOf返回接口動態(tài)值册赛,以reflect.Value的形式返回钠导,與reflect.TypeOf類似:
v := reflect.ValueOf(3)
fmt.Println(v) //"3"
調用Value的Type方法會把它的類型以reflect.Type方式返回:
t := v.Type() //an reflect.Type
fmt.Println(t.String()) //"int"
reflect.ValueOf的逆操作是reflect.Value.Interface方法,它返回一個interface{}接口值森瘪,與reflect.Value包含同一個具體值:
v := reflect.ValueOf(3)
x := v.Interface()
i := x.(int)
fmt.Printf("%d\n", i) //"3"
reflect.Value 與 interface{}都可以包含任意值牡属。二者的區(qū)別是空接口(interface{})隱藏了值的布局信息、內(nèi)置操作和相關方法扼睬,除非我們知道它的動態(tài)類型逮栅,并用一個類型斷言來滲透進去,否則我們對所包含的值能做的事情很少。作為對比证芭,Value有很多方法可以用來分析所包含的值瞳浦,而不用知道它的類型。
利用反射遍歷結構體
前面介紹的是利用反射打印簡單數(shù)據(jù)類型的類型和結構废士,接下來介紹如何用反射遍歷一個結構體叫潦,可以參考下面的一個代碼:
func Display(i interface{}) {
fmt.Printf("Display %T\n", i)
display("", reflect.ValueOf(i))
}
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64,
reflect.Bool,
reflect.String:
fmt.Printf("%s: %v (%s)\n", path, v, v.Type().Name())
case reflect.Ptr:
v = v.Elem()
display(path, v)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
display(" "+path+"["+strconv.Itoa(i)+"]", v.Index(i))
}
case reflect.Struct:
t := v.Type()
for i := 0; i < v.NumField(); i++ {
display(" "+path+"."+t.Field(i).Name, v.Field(i))
}
}
}
首先創(chuàng)建一個Display函數(shù),然后在該函數(shù)里面封裝了一個display官硝,display函數(shù)可以遞歸遍歷結構體內(nèi)部的字段矗蕊,如果是簡單的數(shù)據(jù)類型就直接打印出來,否則就遞歸遍歷結構體成員氢架。接下來簡單解析一下display函數(shù):
數(shù)組與切邊:如果v為數(shù)組或者切邊傻咖,則調用v.Len()函數(shù),獲取該數(shù)組或切邊的長度岖研,調用v.Index(i)卿操, 獲取該數(shù)組或切邊的第i個元素。
指針: 如果v為指針孙援,調用v.Elem(),獲取指針指向的變量害淤,然后遞歸該變量。
結構體:如果v為結構體拓售,v.NumField()將返回該結構體的字段數(shù)窥摄,v.Field(i)將返回第i個字段。
當然础淤,switch語句這里崭放,v.Kind()的類型不僅僅是這點,還有一些其他類型鸽凶,比如reflect.Map币砂,reflect.Interface, reflect.Func吱瘩,reflect.Chan等等道伟,這段代碼的目的僅僅是為了展示如何用反射遍歷結構體,所以沒有必要考慮所有的結構使碾,都是大同小異的。
接下來我們祝懂,我們定義一個內(nèi)嵌結構體的結構體票摇,并且調用Display函數(shù),遍歷該結構體:
type Person struct {
Name string
Age int
Gender bool
}
type Student struct {
Person *Person
Course []string
Core []float32
}
func main() {
st := &Student{
Person: &Person{
Name: "Jim",
Age: 18,
Gender: true,
},
Course: []string{"Math", "Data Structe", "Algorithm"},
Core: []float32{90.5, 85, 89.9},
}
Display(st)
}
輸出如下:
Display *main.Student
.Person.Name: Jim (string)
.Person.Age: 18 (int)
.Person.Gender: true (bool)
.Course[0]: Math (string)
.Course[1]: Data Structe (string)
.Course[2]: Algorithm (string)
.Core[0]: 90.5 (float32)
.Core[1]: 85 (float32)
.Core[2]: 89.9 (float32)
利用reflect.Value修改值
Golang中砚蓬,反射不只用來解析變量值矢门,還可以改變變量的值,接下來介紹如何用reflect.Value來設置變量的值。
我們需要知道的是祟剔,在我們傳參給reflect.ValueOf獲取reflect.Value時隔躲,傳入的是參數(shù)的拷貝,所以reflect.ValueOf獲得的是傳入?yún)?shù)的拷貝的reflect.Value物延,對于它的修改對原來的數(shù)據(jù)是沒有影響的宣旱。所以如果我們想要修改原來的值,就需要用指針叛薯,然后還需要利用Elem方法獲取指針指向的變量浑吟,通過修改Elem()返回的變量,才能真正的修改原始變量的值耗溜,如下:
x := 2
v := reflect.ValueOf(&x)
e := v.Elem()
e.Set(reflect.ValueOf(3))
fmt.Println(x) //"3"
這里组力,還有一些基本類型特化的Set變種:SetInt、SetUint抖拴、SetString燎字、SetFloat等:
e.SetInt(4)
fmt.Println(e) //"4"
在更新變量前,我們可以用CanSet方法來判斷是否可更改阿宅,如果更改一個不可更改的reflect.Value轩触,將導致panic:
fmt.Println(e.CanSet()) //"true"
d := reflect.ValueOf(2)
fmt.Println(d.CanSet()) //"false"
d.Set(reflect.ValueOf(4)) // 這里將導致panic
備注
Golang的反射是i一個功能和表達能力都很強大的工具,但是使用它是要謹慎家夺,具體有如下一些原因:
基于反射的的代碼都很脆弱脱柱。能導致編譯器報告類型錯誤的每種寫法,在反射中都有一個對應的的誤用寫法拉馋。編譯器在編譯時就能向你報告的這個錯誤榨为,而反射錯誤則要等到執(zhí)行時才以崩潰的方式來報告。并且反射還降低了自動重構和分析工具的安全性和準確度煌茴,因為它們無法檢測到類型信息随闺。
我們要避免使用反射的原因是,反射的相關操作無法做靜態(tài)類型檢查蔓腐,所以對于大量使用反射的代碼是很難理解的矩乐。對于接受interfacef{}或者reflect.Value的函數(shù),一定要寫清楚期望的參數(shù)類型和其他限制條件
基于反射的函數(shù)會比特定類型優(yōu)化的函數(shù)慢一兩個數(shù)量級回论。測試很適合用反射散罕,但是對于關鍵路徑上的函數(shù),最好避免使用反射傀蓉。