淺談Golang Reflection (一)

淺談Golang Reflection (一)

什麼是reflection

Golang是一個強態語言,具體來說就是每一個變數都有一個type,而type間的轉換會由compiler強制check。這也就避免了把string賦值給int這種簡單的bug,使compiler成了檢查bug的第一道防線。

可是這種type check並不是沒有代價的,有的時候type check反而會妨礙我們實現一些功能。比如說大家常用的fmt.Print,對於用戶來說我們可以傳入任意數據,而fmt.Print似乎總是能magically列印出傳入的參數。如果我們不能打破type check,而必須手動處理每種type的話這是不現實的。因為我們無法預知用戶會自定義什麼樣的type。從應用層面上來說,reflection的用途就是打破type check的限制。

說到打破type check, 有人可能會想到interface{}。誠然,Golang中任何變數都能賦給interface{},但是這不代表著interface{}就突破了type的限制。假設我們有var a interface{}, a本身並不能做太多的事。但凡我們想讀取a中的任何變數或是invoke任何的函數,都必須先將a做type cast。而做type cast,也就意味著我們必須得知道a可能是什麼type。所以說interface{}雖能在一些函數定義的時候幫助我們寫出更加general的function signature,但是要打破type check還是離不開reflection。

但是,程序員,代價是什麼呢

reflection有著打破type check的能力,可以利用幾行代碼實現非常多的功能。隨之而來的風險也不能忽視。沒有了強制檢查,運用錯type的話程序在運行過程中可能會出現各種panic。而reflection interface的靈活性,使得unit test不一定能測試到所有可能的case。加之reflection會帶來一定的額外開銷。所以一般只有在strong reason的情況下,才會使用reflection。

如何使用reflection

三個代表

package reflect里定義了很多函數。從整體上看需要注意的有三個:func ValueOf(i interface{}) Value, func TypeOf(i interface{}) Typefunc (v Value) Interface() (i interface{})

前兩個函數,把代碼從普通世界帶到了沒有type check的危險世界。而最後的函數,把代碼從reflection的地域又帶回了普通世界。從package的角度來說,這三個函數如同整個package的入口和出口。

兩個基本點

同樣在reflect里的各種類型中,最基本的有兩個:TypeValue

Type提供了檢查Golang類型的方法,可以用來查看一個變數類型方面的信息。

type myStruct struct { field1 int field2 string}var test myStructtestType := reflect.TypeOf(test)fmt.Println(testType.Name()) // 列印type name(myStruct)for i := 0; i < testType.NumField(); i++ { fmt.Println(testType.Field(i).Name) // 列印field name (field1、field2)}

Value則提供了繞開type check,對underlying value進行各種操作的方法。

func reset(input *string) { reflect.ValueOf(input).Elem().SetString("reseted")}testString := "hello"reset(&testString)fmt.Println(testString) //列印reseted

這個例子本身可以不通過reflection實現,在這裡只是作為演示。

幾個例子

// patch函數把diff apply到input上。// input必須是一個指針。// diff的key為input的field名稱, value為更新後的值func patch(input interface{}, diff map[string]interface{}) { entityValue := reflect.Indirect(reflect.ValueOf(input)) for field, value := range diff { fieldValue := entityValue.FieldByName(field) if value == nil { // diff中value為nil,將fielValue設為默認值 fieldValue.Set(reflect.Zero(fieldValue.Type())) } else { fieldValue.Set(reflect.ValueOf(value)) } }}func main() { input := &testStruct{A:10, B:"hello", C:3.14} patch(input, map[string]interface{}{"A": 11, "B": "world", "C": nil}) fmt.Printf("%+v", input) // &{A:11 B:world C:0}, A被更新到11, B被更新到world,C被更新到float64的default值}

上面這個例子不用reflection很難寫出相似功能的代碼。這個func對auto generated code額外有用(比如proto, thrift)。如果不用reflection,我們可以針對不同的input type編寫不同的func,但是維護的難度很大。每次增減input type中的field都必須相應的更新patch。

或者我們可以用template或其他技術針對不同的type自動生成patch代碼(類似mockgen),這在一些情況下可能是更加preferred的做法。但是相應的開發工作會多很多。

type queue struct { ExpectedType interface{} slice []interface{}}func (q *queue) Push(v interface{}) error { if reflect.TypeOf(v) != reflect.TypeOf(q.ExpectedType){ return fmt.Errorf("unexpected type") } q.slice = append(q.slice, v) return nil}func main() { q := &queue{ExpectedType:""} //創建一個只接受string的queue fmt.Println(q.Push("1")) // No error fmt.Println(q.Push(1)) // 類型錯誤}

第二個例子在一些開源軟體里的底層code能常看到。因為底層code需要被不同的component所調用,所以一般都會設計的比較general。如果一個類對所處理的type有要求的話,一般來說Generic是最好的選擇。

在Golang不支持Generic的情況下,一些repo就用reflect來做一個簡化版的Generic。雖然不能在compile的時候就發現問題,但也好過一點check都不做。實際的repo中,可能會用reflect來做更多的檢查(比如type間是否能轉換,interface的話是否實現),但是基本思路都和queue一致。

一點人生的經驗

reflection大概的用法在上面基本都提過了。但是在實際使用過程中,經常會在改變Value值得時候觸發panic。除了類型不符外,最常見的問題就是Value不是assignable。

第一點,Value所含的值必須從從指針或者slice/array而來。以第一個例子來說,若果input := testStruct{A:10, B:"hello", C:3.14}而不是input := &testStruct{A:10, B:"hello", C:3.14}的話,patch是會報錯的。

第二點,Value.Elem/reflect.Indirect使用不當。在第一個例子中,我們用了entityValue := reflect.Indirect(reflect.ValueOf(input)),如果沒有reflect.Indirect的話,patch也會報錯。究其原因,reflect.Value(input)返回的是一個指針的Value,而指針本身reset值是沒有意義的。只有指針所指向的值才能被更改。reflect.Indirect返回的Value包含的便是指針所包含的值。reflect.ValueOf(/**指針**/)就好比var ptr *intptr = /*some value*/並沒有意義。而reflect.Indirect返回的是*ptr,更改*ptr便更改了ptr所指向的值。Value.Elem/reflect.Indirect在一些方面類似,具體請參考文檔。

以上兩點可總結為Addressable。Addressable具體的意思可以參看Value.CanSet函數的Comment。

Next

這篇文章大概介紹了reflection的用法,可是為什麼reflection能打破type check呢?在下一篇文章,我會介紹reflection的核心unsafe.Pointer


推薦閱讀:

各個航空公司代碼
手把手,帶你給Swift代碼添加註釋語句
css防止圖片撐破頁面的代碼(圖片自動縮放)
ldm2124086口袋代碼
jQuery 1.4十大新特性解讀及代碼示例

TAG:Go語言 | 編程 | 代碼 |