技巧:Go 結(jié)構(gòu)體如何轉(zhuǎn)換成 map[string]interface{}
本文介紹了Go語言中將結(jié)構(gòu)體轉(zhuǎn)成map[string]interface{}時你需要了解的“坑”,也有你需要知道的若干方法。
我們在Go語言中通常使用結(jié)構(gòu)體來保存我們的數(shù)據(jù),例如要存儲用戶信息,我們可能會定義如下結(jié)構(gòu)體:
// UserInfo 用戶信息type UserInfo struct {Name string `json:"name"`Age int `json:"age"`}u1 := UserInfo{Name: "q1mi", Age: 18}
假設(shè)現(xiàn)在要將上面的u1轉(zhuǎn)換成map[string]interface{},該如何操作呢?
結(jié)構(gòu)體轉(zhuǎn)map[string]interface{}
JSON序列化方式
這不是很簡單嗎?我用json序列化一下u1,再反序列化成map不就可以了么。說干就干,代碼如下:
func main() {u1 := UserInfo{Name: "q1mi", Age: 18}b, _ := json.Marshal(&u1)var m map[string]interface{}_ = json.Unmarshal(b, &m)for k, v := range m{fmt.Printf("key:%v value:%v\n", k, v)}}
輸出:
key:name value:q1mikey:age value:18
看起來沒什么問題,但其實這里是有一個“坑”的。那就是Go語言中的json包在序列化空接口存放的數(shù)字類型(整型、浮點型等)都會序列化成float64類型。
也就是上面例子中m["age"]現(xiàn)在底層是一個float64了,不是個int了。我們來驗證下:
func main() {u1 := UserInfo{Name: "q1mi", Age: 18}b, _ := json.Marshal(&u1)var m map[string]interface{}_ = json.Unmarshal(b, &m)for k, v := range m{fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)}}
輸出:
key:name value:q1mi value type:stringkey:age value:18 value type:float64
很顯然,出現(xiàn)了一個意料之外的行為,我們得想辦法規(guī)避掉。
反射
沒辦法就需要自己動手去實現(xiàn)了。這里使用反射遍歷結(jié)構(gòu)體字段的方式生成map, 具體代碼如下:
// ToMap 結(jié)構(gòu)體轉(zhuǎn)為Map[string]interface{}func ToMap(in interface{}, tagName string) (map[string]interface{}, error){out := make(map[string]interface{})v := reflect.ValueOf(in)if v.Kind() == reflect.Ptr {v = v.Elem()}if v.Kind() != reflect.Struct { // 非結(jié)構(gòu)體返回錯誤提示return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %T", v)}t := v.Type()// 遍歷結(jié)構(gòu)體字段// 指定tagName值為map中key;字段值為map中valuefor i := 0; i < v.NumField(); i++ {fi := t.Field(i)if tagValue := fi.Tag.Get(tagName); tagValue != "" {out[tagValue] = v.Field(i).Interface()}}return out, nil}
驗證一下:
m2, _ := ToMap(&u1, "json")for k, v := range m2{fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)}
輸出:
key:name value:q1mi value type:stringkey:age value:18 value type:int
這一次map["age"]的類型就對了的。
第三方庫structs
除了自己使用反射實現(xiàn),Github上也有現(xiàn)成的輪子,例如第三方庫:https://github.com/fatih/structs。
這個包使用的自定義結(jié)構(gòu)體tag是structs:
// UserInfo 用戶信息type UserInfo struct {Name string `json:"name" structs:"name"`Age int `json:"age" structs:"age"`}
用法很簡單:
m3 := structs.Map(&u1)for k, v := range m3 {fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)}
structs這個包也有很多其他的使用示例,大家可以去查看文檔。但是需要注意的是目前這個庫已經(jīng)被作者設(shè)置為只讀了。
嵌套結(jié)構(gòu)體轉(zhuǎn)map[string]interface{}
structs本身是支持嵌套結(jié)構(gòu)體轉(zhuǎn)map[string]interface{}的,遇到結(jié)構(gòu)體嵌套它會轉(zhuǎn)換為map[string]interface{}嵌套map[string]interface{}的模式。
我們定義一組嵌套的結(jié)構(gòu)體如下:
// UserInfo 用戶信息type UserInfo struct {Name string `json:"name" structs:"name"`Age int `json:"age" structs:"age"`Profile `json:"profile" structs:"profile"`}// Profile 配置信息type Profile struct {Hobby string `json:"hobby" structs:"hobby"`}
聲明結(jié)構(gòu)體變量u1:
u1 := UserInfo{Name: "q1mi", Age: 18, Profile: Profile{"雙色球"}}第三方庫structs
轉(zhuǎn)換嵌套結(jié)構(gòu)體的代碼和上面其實是一樣的:
m3 := structs.Map(&u1)for k, v := range m3 {fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)}
輸出結(jié)果:
key:name value:q1mi value type:stringkey:age value:18 value type:intkey:profile value:map[hobby:雙色球] value type:map[string]interface {}
從結(jié)果來看最后嵌套字段profile是map[string]interface {},屬于map嵌套map。
使用反射轉(zhuǎn)成單層map
如果我們想把嵌套的結(jié)構(gòu)體轉(zhuǎn)換成一個單層map該怎么做呢?
我們只需要把上面反射的代碼稍微修改一下就可以了,修改后的代碼如下:
// ToMap2 將結(jié)構(gòu)體轉(zhuǎn)為單層mapfunc ToMap2(in interface{}, tag string) (map[string]interface{}, error) {// 當(dāng)前函數(shù)只接收struct類型v := reflect.ValueOf(in)if v.Kind() == reflect.Ptr { // 結(jié)構(gòu)體指針v = v.Elem()}if v.Kind() != reflect.Struct {return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %T", v)}out := make(map[string]interface{}, 8)queue := make([]interface{}, 0, 2)queue = append(queue, in)for len(queue) > 0 {v := reflect.ValueOf(queue[0])if v.Kind() == reflect.Ptr { // 結(jié)構(gòu)體指針v = v.Elem()}queue = queue[1:]t := v.Type()for i := 0; i < v.NumField(); i++ {vi := v.Field(i)if vi.Kind() == reflect.Ptr { // 內(nèi)嵌指針vi = vi.Elem()if vi.Kind() == reflect.Struct { // 結(jié)構(gòu)體queue = append(queue, vi.Interface())} else {ti := t.Field(i)if tagValue := ti.Tag.Get(tag); tagValue != "" {// 存入mapout[tagValue] = vi.Interface()}}break}if vi.Kind() == reflect.Struct { // 內(nèi)嵌結(jié)構(gòu)體queue = append(queue, vi.Interface())break}// 一般字段ti := t.Field(i)if tagValue := ti.Tag.Get(tag); tagValue != "" {// 存入mapout[tagValue] = vi.Interface()}}}return out, nil}
測試一下:
m4, _ := ToMap2(&u1, "json")for k, v := range m4 {fmt.Printf("key:%v value:%v value type:%T\n", k, v, v)}
輸出:
key:name value:q1mi value type:stringkey:age value:18 value type:intkey:hobby value:雙色球 value type:string
這下我們就把嵌套的結(jié)構(gòu)體轉(zhuǎn)為單層的map了,但是要注意這種場景下結(jié)構(gòu)體和嵌套結(jié)構(gòu)體的字段就需要避免重復(fù)。
