瞭解和使用golang有一段時間了,由於項目比較趕,基本是現學現賣的節奏。最近有時間會在簡書上記錄遇到的一些問題和解決方案,希望可以一起交流探討。
需求
- 在golang中,給定一組數據,例如
map[string]interface{}
類型的數據,創建一個對應的struct並賦值
簡易實現
var data = map[string]interface{}{ "id": 1001, "name": "apple", "price": 16.25, } type Fruit struct { ID int Name string Price float64 } func newFruit(data map[string]interface{}) *Fruit { s := Fruit{ ID: data["id"].(int), Name: data["name"].(string), Price: data["price"].(float64), } return &s } func main() { fruit := newFruit(data) log.Println("fruit:", fruit) }
> fruit: &{1001 apple 16.25}
這樣實現簡單快速,但也有缺點:
- 難以維護,每次新增字段都要修改newFruit函數
- 不夠優雅,需要手動對每一個字段進行賦值和類型轉換
- 不夠通用,只能創建欽定的struct
改進
是否有更好的解決方法,自動遍歷struct對象,並進行賦值呢?
首先想到for...range操作符,但golang裏range無法對結構體進行遍歷。
(如果只需遍歷struct而不用賦值,可以嘗試邪道組合: json.Marshal()
和 json.Unmarshal()
一鍵把struct轉成 map[string]interface()
)
實際上要遍歷一個struct,需要使用golang的reflect包。關於golang的反射機制不再贅述,可以參考go的文檔,有很詳細的說明。
那麼現在利用reflect,嘗試改進之前的代碼
var data = map[string]interface{}{ "id": 1001, "name": "apple", "price": 16.25, } type Fruit struct { ID int Name string Price float64 } // 遍歷struct並且自動進行賦值 func structByReflect(data map[string]interface{}, inStructPtr interface{}) { rType := reflect.TypeOf(inStructPtr) rVal := reflect.ValueOf(inStructPtr) if rType.Kind() == reflect.Ptr { // 傳入的inStructPtr是指針,需要.Elem()取得指針指向的value rType = rType.Elem() rVal = rVal.Elem() } else { panic("inStructPtr must be ptr to struct") } // 遍歷結構體 for i := 0; i < rType.NumField(); i++ { t := rType.Field(i) f := rVal.Field(i) if v, ok := data[t.Name]; ok { f.Set(reflect.ValueOf(v)) } else { panic(t.Name + " not found") } } } func main() { //fruit := newFruit(data) fruit := Fruit{} structByReflect(data, &fruit) log.Println("fruit:", fruit) }
編譯運行
> panic: ID not found
新的問題出現了,結構體的字段名 ID
和data中的 id
大小寫不一致,導致無法從data中取得對應的數據。
修改data的key name,或者修改struct的field name當然可以解決,但在實際應用中,data往往從外部獲得不受控制,而data的key通常也不符合go的命名規範,因此暴力改名不可取。
那怎麼解決呢?這裏可以利用go的 成員變量標籤(field tag) ,給struct的字段增加額外的元數據,用以指定對應的字段名。golang對json和xml等的序列化處理也是用了這個方法。
type Fruit struct { ID int `key:"id"` Name string `key:"name"` Price float64 `key:"price"` } // 遍歷struct並且自動進行賦值 func structByReflect(data map[string]interface{}, inStructPtr interface{}) { rType := reflect.TypeOf(inStructPtr) rVal := reflect.ValueOf(inStructPtr) if rType.Kind() == reflect.Ptr { // 傳入的inStructPtr是指針,需要.Elem()取得指針指向的value rType = rType.Elem() rVal = rVal.Elem() } else { panic("inStructPtr must be ptr to struct") } // 遍歷結構體 for i := 0; i < rType.NumField(); i++ { t := rType.Field(i) f := rVal.Field(i) // 得到tag中的字段名 key := t.Tag.Get("key") if v, ok := data[key]; ok { f.Set(reflect.ValueOf(v)) } else { panic(t.Name + " not found") } } }
再次編譯運行,這次得到了期望的結果
> fruit: {1001 apple 16.25}
類型轉換問題
到這裏已經基本實現了想要的功能,但還有一個問題,如果data中的數據類型,和struct中定義的類型稍有不一致,反射賦值語句就會報錯,
var data = map[string]interface{}{ "id": 1001, "name": "apple", "price": 16, // 改成int類型 }
測試一下:
> panic: reflect.Set: value of type int is not assignable to type float64
我們知道 int
和 float64
可以相互強制轉換,但是 reflect.Set()
方法並不想幫你轉。
這裏還是要利用reflect包的兩個方法, Type.ConvertibleTo(u Type)
用來判斷能否轉換到指定類型,再通過 Value.Convert(t Type)
來進行類型轉換。
再次優化我們的函數:
// 遍歷struct並且自動進行賦值 func structByReflect(data map[string]interface{}, inStructPtr interface{}) { rType := reflect.TypeOf(inStructPtr) rVal := reflect.ValueOf(inStructPtr) if rType.Kind() == reflect.Ptr { // 傳入的inStructPtr是指針,需要.Elem()取得指針指向的value rType = rType.Elem() rVal = rVal.Elem() } else { panic("inStructPtr must be ptr to struct") } // 遍歷結構體 for i := 0; i < rType.NumField(); i++ { t := rType.Field(i) f := rVal.Field(i) // 得到tag中的字段名 key := t.Tag.Get("key") if v, ok := data[key]; ok { // 檢查是否需要類型轉換 dataType := reflect.TypeOf(v) structType := f.Type() if structType == dataType { f.Set(reflect.ValueOf(v)) } else { if dataType.ConvertibleTo(structType) { // 轉換類型 f.Set(reflect.ValueOf(v).Convert(structType)) } else { panic(t.Name + " type mismatch") } } } else { panic(t.Name + " not found") } } }
在f.Set()之前,先檢查data的Type和struct字段的Type是否一致,如果不一致則進行轉換。
> fruit: {1001 apple 16}
這樣功能就全部完成了,示例代碼中遇到錯誤都直接拋出panic,可以根據實際項目進行調整。
主要到這裏沒有處理嵌套的結構體等情況,這部分通過判斷Type爲struct時,進行遞歸處理就可以實現。