這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
golang反射機制介紹
Go語言提供了一種機制,在編譯時間不知道類型的情況下,可更新變數、在運行時查看值、調用方法以及直接對他們的布局進行操作,這種機制稱為反射(reflection)。
reflect.Type 和 reflect.Value
反射功能由reflect包提供,它定義了兩個重要的類型:Type 和 Value,分別表示變數的類型和值。反射包裡面,提供了reflect.TypeOf 和 reflect.ValueOf,返回被檢查對象的Type 和 Value:
func TypeOf(i interface{}) Typefunc ValueOf(i interface{}) Value
可以看到,TypeOf和ValueOf的參數是interface{}類型,當把一個具體值賦給一個interface{}類型時,會發生一個隱式的類型轉換,轉換會產生一個包含兩個部分內容的介面值:動態類型部分是運算元的類型,動態值部分是運算元的值。
reflect.TypeOf返回介面值對應的動態類型,注意返回的時具體實值型別而不是介面類型。比如下面的輸出的是"*os.File"而不是"io.Writer":
var w io.Writer = os.Stdoutfmt.Println(reflect.TypeOf(v)) // "*os.File"
同理,reflect.ValueOf返回介面動態值,以reflect.Value的形式返回,與reflect.TypeOf類似:
v := reflect.ValueOf(3)fmt.Println(v) //"3"
調用Value的Type方法會把它的類型以reflect.Type方式返回:
t := v.Type() //an reflect.Typefmt.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{}都可以包含任意值。二者的區別是空介面(interface{})隱藏了值的布局資訊、內建操作和相關方法,除非我們知道它的動態類型,並用一個類型斷言來滲透進去,否則我們對所包含的值能做的事情很少。作為對比,Value有很多方法可以用來分析所包含的值,而不用知道它的類型。
利用反射遍曆結構體
前面介紹的是利用反射列印單一資料型別的類型和結構,接下來介紹如何用反射遍曆一個結構體,可以參考下面的一個代碼:
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)) } } }
首先建立一個Display函數,然後在該函數裡面封裝了一個display,display函數可以遞迴遍曆結構體內部的欄位,如果是簡單的資料類型就直接列印出來,否則就遞迴遍曆結構體成員。接下來簡單解析一下display函數:
數組與切邊:如果v為數組或者切邊,則調用v.Len()函數,擷取該數組或切邊的長度,調用v.Index(i), 擷取該數組或切邊的第i個元素。
指標: 如果v為指標,調用v.Elem(),擷取指標指向的變數,然後遞迴該變數。
結構體:如果v為結構體,v.NumField()將返回該結構體的欄位數,v.Field(i)將返回第i個欄位。
當然,switch語句這裡,v.Kind()的類型不僅僅是這點,還有一些其他類型,比如reflect.Map,reflect.Interface, reflect.Func,reflect.Chan等等,這段代碼的目的僅僅是為了展示如何用反射遍曆結構體,所以沒有必要考慮所有的結構,都是大同小異的。
接下來我們,我們定義一個內嵌結構體的結構體,並且調用Display函數,遍曆該結構體:
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時,傳入的是參數的拷貝,所以reflect.ValueOf獲得的是傳入參數的拷貝的reflect.Value,對於它的修改對原來的資料是沒有影響的。所以如果我們想要修改原來的值,就需要用指標,然後還需要利用Elem方法擷取指標指向的變數,通過修改Elem()返回的變數,才能真正的修改原始變數的值,如下:
x := 2v := 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一個功能和表達能力都很強大的工具,但是使用它是要謹慎,具體有如下一些原因:
基於反射的的代碼都很脆弱。能導致編譯器報告類型錯誤的每種寫法,在反射中都有一個對應的的誤用寫法。編譯器在編譯時間就能向你報告的這個錯誤,而反射錯誤則要等到執行時才以崩潰的方式來報告。並且反射還降低了自動重構和分析工具的安全性和準確度,因為它們無法檢測到類型資訊。
我們要避免使用反射的原因是,反射的相關操作無法做靜態類型檢查,所以對於大量使用反射的代碼是很難理解的。對於接受interfacef{}或者reflect.Value的函數,一定要寫清楚期望的參數類型和其他限制條件
基於反射的函數會位元定類型最佳化的函數慢一兩個數量級。測試很適合用反射,但是對於關鍵路徑上的函數,最好避免使用反射。