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ā)生一個隱式的類型轉換,轉換會生成一個包含兩個部分內容的接口值:動態(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{})隱藏了值的布局信息、內置操作和相關方法,除非我們知道它的動態(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ù)可以遞歸遍歷結構體內部的字段,如果是簡單的數(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等等,這段代碼的目的僅僅是為了展示如何用反射遍歷結構體,所以沒有必要考慮所有的結構,都是大同小異的。
接下來我們,我們定義一個內嵌結構體的結構體,并且調用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ù),最好避免使用反射。