這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
前言
因為 golang 靜態強型別語言特性以及沒有很好的泛型支援導致在用 go 寫 web 服務的時候,總會因為要對 http params 的解析和類型轉換上要花很多時間,並且這會讓代碼顯得很冗餘,那有什麼辦法可以解決這一苦痛呢?答案當然是有的,這裡我講會到如何用 reflect
包寫一個工具類實現 model 層 struct 與 http params 的自動對應綁定。
具體實現其實很簡單,主要用到的就是通過 reflect.TypeOf()
擷取欄位的類型(包括欄位名,類型,Tag描述等相關資訊),以及 reflect.ValueOf()
來擷取欄位的實值型別用於複寫從params擷取到的資料, 同時還要注意不同類型數值在 Set
時的差別。
用料
首先我們設計一個struct來儲存每個反射欄位的屬性,就比如以下這樣。
注意:取決於 golang 對於反射模型實現上的差異,這種操作在 go 裡面其實並不是那麼的高效,推薦在第一次反射後 cache 一份結果到記憶體,以便下次用的時候直接擷取。
type field struct {name stringdef booldefValue reflect.Valuerequired bool}
通過 range
reflect.Type 擷取 struct field 資訊並填充到 []*field
,其中這包括了欄位是否必傳、預設值、欄位名,這些都可以利用自訂 TAG 描述實現。
type Account struct {Email string `params:"email;required"`Name string `params:"name;required"`Sign string `params:"sign"`San int `params:"sam" defalut:"-99999"`}
除此之外我們還得需要一個 bitmap 用於映射 reflect.Kind
對應著的類型關係,以便於在 Set Value 時做好類型轉換
var bitMap = map[reflect.Kind]int{reflect.Int.Stringreflect.Int: 32,reflect.Int16: 16,reflect.Int32: 32,reflect.Int64: 64,reflect.Int8: 8,reflect.Uint: 32,reflect.Uint16: 16,reflect.Uint32: 32,reflect.Uint64: 64,reflect.Uint8: 8,reflect.Float32: 32,reflect.Float64: 64,}
實現
當完備以上材料後,想要實現我們的功能那就如魚得水輕鬆自如了,只需要 range
我們定義好的 field slice,依次從 url.Values
中 Get 參數中的值,因為 http 協議的請求報文是面向文本沒有額外的類型描述,因此我們擷取到的資料都是文本類型,這時候我們需要根據 reflect.Kind
以不同類型調用不同 Set
方法。
func setValue(data string, v reflect.Value) (err error) {kind := v.Kind()switch kind {case reflect.Int64, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int:var d int64d, err = strconv.ParseInt(data, 10, bitMap[kind])if err != nil {return}v.SetInt(d)case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint8:var d uint64d, err = strconv.ParseUint(data, 10, bitMap[kind])if err != nil {return}v.SetUint(d)case reflect.Float64, reflect.Float32:var d float64d, err = strconv.ParseFloat(data, bitMap[kind])if err != nil {return}v.SetFloat(d)case reflect.String:if data == "" {return}v.SetString(data)return
reflect 效能最佳化
reflect慢主要有兩個原因:一是涉及到記憶體配置 malloc
以後的 GC(這在Go 1.9 Vsersion 中有所改善);二是 reflect 實現裡面有大量的枚舉,以及類型推導之類的,再者 reflect.ValueOf()
得到的 reflect.Value
類型是一個具體的值,每次反射都得需要重新 malloc
這就又拖慢了整個過程的速度。
那如果我們不使用 reflect.ValueOf()
得到值,直接使用 reflect.TypeOf()
的結果操作 struct
的資料,省掉一層反射是不是速度就會快很多呢? 答案當然是一定的!直接上代碼~
func setValueWithPointer() {acc := &Account{}tp := reflect.TypeOf(acc).Elem()field, _ := tp.FieldByName("Email")fieldPtr := uintptr(unsafe.Pointer(acc)) + field.Offset*((*string)(unsafe.Pointer(fieldPtr))) = "admin#otokaze.cn"fmt.Println(acc) // stdout: &{admin#otokaze.cn 0}}
我們很巧妙的利用了 reflect.TypeOf()
預留給我們的 struct 內部 field 記憶體位址的位移量,也因為 uintptr()
強轉得到的是一個整形記憶體位址,這是可以進行算術運算的,只要拿到初始化 struct 後分配開始的記憶體位址再加上 field 記憶體位址的位移量,我們就能直接拿到這個 field 在實體記憶體上的地址,以此來寫入我們需要的內容。這種最直接的方式也節省了 reflect.ValueOf()
做的二次反射,同時也達到了我們的修改目的。
以上,由此可見只要掌握了正確的姿勢,golang 的反射效率依舊可以有很大提升!反射的應用情境還遠不只如此,我們都知道因為靜態語言的關係在 golang 沒有如同 php 中 $$
可變變數的支援,其實也可以通過反射來實作類別似的效果,不過這就不是今天這篇文章所屬的範疇了,把它當作知識點,循循善誘。還有更多的技巧等著你發現~