golang使用mapstructure解析json
背景
前幾天群里的小伙伴問了一個這樣的問題:
其實質(zhì)就是在面對 value
類型不確定的情況下,怎么解析這個 json?
我下意識就想到了 [mapstructure](https://github.com/mitchellh/mapstructure)
這個庫,它可以幫助我們類似 PHP 那樣去處理弱類型的結(jié)構(gòu)。
介紹
先來介紹一下 mapstructure
這個庫主要用來做什么的吧,官網(wǎng)是這么介紹的:
mapstructure
是一個 Go 庫,用于將通用映射值解碼為結(jié)構(gòu),反之亦然,同時提供有用的錯誤處理。
該庫在解碼數(shù)據(jù)流(JSON、Gob 等)中的值時最為有用,因為在讀取部分?jǐn)?shù)據(jù)之前,您并不十分清楚底層數(shù)據(jù)的結(jié)構(gòu)。因此,您可以讀取 map[string]interface{}
并使用此庫將其解碼為適當(dāng)?shù)谋镜?Go 底層結(jié)構(gòu)。
簡單來說,它擅長解析一些我們并不十分清楚底層數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)流到我們定義的結(jié)構(gòu)體中。
下面我們通過幾個例子來簡單介紹一下 mapstructure
怎么使用。
例子
普通形式
func normalDecode() { type Person struct { Name string Age int Emails []string Extra map[string]string } // 此輸入可以來自任何地方,但通常來自諸如解碼 JSON 之類的東西,我們最初不太確定結(jié)構(gòu)。 input := map[string]interface{}{ "name": "Tim", "age": 31, "emails": []string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, "extra": map[string]string{ "twitter": "Tim", }, } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v\n", result) }
輸出:
main.Person{Name:"Tim", Age:31, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Tim"}}
這個方式應(yīng)該是我們最經(jīng)常使用的,非常簡單的將 map[string]interface{}
映射到我們的結(jié)構(gòu)體中。
在這里,我們并沒有指定每個 field
的 tag
,讓 mapstructure
自動去映射。
如果我們的 input
是一個 json 字符串,那么我們需要將 json 字符串解析為 map[string]interface{}
之后,再將其映射到我們的結(jié)構(gòu)體中。
func jsonDecode() { var jsonStr = `{ "name": "Tim", "age": 31, "gender": "male" }` type Person struct { Name string Age int Gender string } m := make(map[string]interface{}) err := json.Unmarshal([]byte(jsonStr), &m) if err != nil { panic(err) } var result Person err = mapstructure.Decode(m, &result) if err != nil { panic(err.Error()) } fmt.Printf("%#v\n", result) }
輸出:
main.Person{Name:"Tim", Age:31, Gender:"male"}
嵌入式結(jié)構(gòu)
mapstructure
允許我們壓縮多個嵌入式結(jié)構(gòu),并通過 squash
標(biāo)簽進(jìn)行處理。
func embeddedStructDecode() { // 使用 squash 標(biāo)簽允許壓縮多個嵌入式結(jié)構(gòu)。通過創(chuàng)建多種類型的復(fù)合結(jié)構(gòu)并對其進(jìn)行解碼來演示此功能。 type Family struct { LastName string } type Location struct { City string } type Person struct { Family `mapstructure:",squash"` Location `mapstructure:",squash"` FirstName string } input := map[string]interface{}{ "FirstName": "Tim", "LastName": "Liu", "City": "China, Guangdong", } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%s %s, %s\n", result.FirstName, result.LastName, result.City) }
輸出:
Tim Liu, China, Guangdong
在這個例子中, Person
里面有著 Location
和 Family
的嵌入式結(jié)構(gòu)體,通過 squash
標(biāo)簽進(jìn)行壓縮,從而達(dá)到平鋪的作用。
元數(shù)據(jù)
func metadataDecode() { type Person struct { Name string Age int Gender string } // 此輸入可以來自任何地方,但通常來自諸如解碼 JSON 之類的東西,我們最初不太確定結(jié)構(gòu)。 input := map[string]interface{}{ "name": "Tim", "age": 31, "email": "one@gmail.com", } // 對于元數(shù)據(jù),我們制作了一個更高級的 DecoderConfig,以便我們可以更細(xì)致地配置所使用的解碼器。在這種情況下,我們只是告訴解碼器我們想要跟蹤元數(shù)據(jù)。 var md mapstructure.Metadata var result Person config := &mapstructure.DecoderConfig{ Metadata: &md, Result: &result, } decoder, err := mapstructure.NewDecoder(config) if err != nil { panic(err) } if err = decoder.Decode(input); err != nil { panic(err) } fmt.Printf("value: %#v, keys: %#v, Unused keys: %#v, Unset keys: %#v\n", result, md.Keys, md.Unused, md.Unset) }
輸出:
value: main.Person{Name:"Tim", Age:31, Gender:""}, keys: []string{"Name", "Age"}, Unused keys: []string{"email"}, Unset keys: []string{"Gender"}
從這個例子我們可以看出,使用 Metadata
可以記錄我們結(jié)構(gòu)體以及 map[string]interface{}
的差異,相同的部分會正確映射到對應(yīng)的字段中,而差異則使用了 Unused
和 Unset
來表達(dá)。
- Unused:map 中有著結(jié)構(gòu)體所沒有的字段。
- Unset:結(jié)構(gòu)體中有著 map 中所沒有的字段。
避免空值的映射
這里的使用其實和內(nèi)置的 json 庫使用方式是一樣的,都是借助 omitempty
標(biāo)簽來解決。
func omitemptyDecode() { // 添加 omitempty 注釋以避免空值的映射鍵 type Family struct { LastName string } type Location struct { City string } type Person struct { *Family `mapstructure:",omitempty"` *Location `mapstructure:",omitempty"` Age int FirstName string } result := &map[string]interface{}{} input := Person{FirstName: "Somebody"} err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%+v\n", result) }
輸出:
&map[Age:0 FirstName:Somebody]
這里我們可以看到 *Family
和 *Location
都被設(shè)置了 omitempty
,所以在解析過程中會忽略掉空值。而 Age
沒有設(shè)置,并且 input
中沒有對應(yīng)的 value
,所以在解析中使用對應(yīng)類型的零值來表達(dá),而 int
類型的零值就是 0
。
剩余字段
func remainDataDecode() { type Person struct { Name string Age int Other map[string]interface{} `mapstructure:",remain"` } input := map[string]interface{}{ "name": "Tim", "age": 31, "email": "one@gmail.com", "gender": "male", } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v\n", result) }
輸出:
main.Person{Name:"Tim", Age:31, Other:map[string]interface {}{"email":"one@gmail.com", "gender":"male"}}
從代碼可以看到 Other
字段被設(shè)置了 remain
,這意味著 input
中沒有正確映射的字段都會被放到 Other
中,從輸出可以看到,email
和 gender
已經(jīng)被正確的放到 Other
中了。
自定義標(biāo)簽
func tagDecode() { // 請注意,結(jié)構(gòu)類型中定義的 mapstructure 標(biāo)簽可以指示將值映射到哪些字段。 type Person struct { Name string `mapstructure:"person_name"` Age int `mapstructure:"person_age"` } input := map[string]interface{}{ "person_name": "Tim", "person_age": 31, } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v\n", result) }
輸出:
main.Person{Name:"Tim", Age:31}
在 Person
結(jié)構(gòu)中,我們將 person_name
和 person_age
分別映射到 Name
和 Age
中,從而達(dá)到在不破壞結(jié)構(gòu)的基礎(chǔ)上,去正確的解析。
弱類型解析
正如前面所說,mapstructure
提供了類似 PHP 解析弱類型結(jié)構(gòu)的方法。
func weaklyTypedInputDecode() { type Person struct { Name string Age int Emails []string } // 此輸入可以來自任何地方,但通常來自諸如解碼 JSON 之類的東西,由 PHP 等弱類型語言生成。 input := map[string]interface{}{ "name": 123, // number => string "age": "31", // string => number "emails": map[string]interface{}{}, // empty map => empty array } var result Person config := &mapstructure.DecoderConfig{ WeaklyTypedInput: true, Result: &result, } decoder, err := mapstructure.NewDecoder(config) if err != nil { panic(err) } err = decoder.Decode(input) if err != nil { panic(err) } fmt.Printf("%#v\n", result) }
輸出:
main.Person{Name:"123", Age:31, Emails:[]string{}}
從代碼可以看到,input
中的 name
、age
和 Person
結(jié)構(gòu)體中的 Name
、Age
類型不一致,而 email
更是離譜,一個字符串?dāng)?shù)組,一個是 map
。
但是我們通過自定義 DecoderConfig
,將 WeaklyTypedInput
設(shè)置成 true
之后,mapstructure
很容易幫助我們解決這類弱類型的解析問題。
但是也不是所有問題都能解決,通過源碼我們可以知道有如下限制:
// - bools to string (true = "1", false = "0") // - numbers to string (base 10) // - bools to int/uint (true = 1, false = 0) // - strings to int/uint (base implied by prefix) // - int to bool (true if value != 0) // - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F, // FALSE, false, False. Anything else is an error) // - empty array = empty map and vice versa // - negative numbers to overflowed uint values (base 10) // - slice of maps to a merged map // - single values are converted to slices if required. Each // element is weakly decoded. For example: "4" can become []int{4} // if the target type is an int slice.
大家使用這種弱類型解析的時候也需要注意。
錯誤處理
mapstructure
錯誤提示非常的友好,下面我們來看看遇到錯誤時,它是怎么提示的。
func decodeErrorHandle() { type Person struct { Name string Age int Emails []string Extra map[string]string } input := map[string]interface{}{ "name": 123, "age": "bad value", "emails": []int{1, 2, 3}, } var result Person err := mapstructure.Decode(input, &result) if err != nil { fmt.Println(err.Error()) } }
輸出:
5 error(s) decoding:
* 'Age' expected type 'int', got unconvertible type 'string', value: 'bad value'
* 'Emails[0]' expected type 'string', got unconvertible type 'int', value: '1'
* 'Emails[1]' expected type 'string', got unconvertible type 'int', value: '2'
* 'Emails[2]' expected type 'string', got unconvertible type 'int', value: '3'
* 'Name' expected type 'string', got unconvertible type 'int', value: '123'
這里的錯誤提示會告訴我們每個字段,字段里的值應(yīng)該需要怎么表達(dá),我們可以通過這些錯誤提示,比較快的去修復(fù)問題。
總結(jié)
從上面這些例子看看到 mapstructure 的強大之處,很好的幫我們解決了實實在在的問題,也在節(jié)省我們的開發(fā)成本。
但是從源碼來看,內(nèi)部使用了大量的反射,這可能會對一些特殊場景帶來性能隱患。所以大家在使用的時候,一定要充分考慮產(chǎn)品邏輯以及場景。
以下貼一小段刪減過的源碼:
// Decode decodes the given raw interface to the target pointer specified // by the configuration. func (d *Decoder) Decode(input interface{}) error { return d.decode("", input, reflect.ValueOf(d.config.Result).Elem()) } // Decodes an unknown data type into a specific reflection value. func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error { .... var err error outputKind := getKind(outVal) addMetaKey := true switch outputKind { case reflect.Bool: err = d.decodeBool(name, input, outVal) case reflect.Interface: err = d.decodeBasic(name, input, outVal) case reflect.String: err = d.decodeString(name, input, outVal) case reflect.Int: err = d.decodeInt(name, input, outVal) case reflect.Uint: err = d.decodeUint(name, input, outVal) case reflect.Float32: err = d.decodeFloat(name, input, outVal) case reflect.Struct: err = d.decodeStruct(name, input, outVal) case reflect.Map: err = d.decodeMap(name, input, outVal) case reflect.Ptr: addMetaKey, err = d.decodePtr(name, input, outVal) case reflect.Slice: err = d.decodeSlice(name, input, outVal) case reflect.Array: err = d.decodeArray(name, input, outVal) case reflect.Func: err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it return fmt.Errorf("%s: unsupported type: %s", name, outputKind) } // If we reached here, then we successfully decoded SOMETHING, so // mark the key as used if we're tracking metainput. if addMetaKey && d.config.Metadata != nil && name != "" { d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) } return err }
到此這篇關(guān)于golang使用mapstructure解析json的文章就介紹到這了,更多相關(guān)go mapstructure解析json內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
更換GORM默認(rèn)SQLite驅(qū)動出現(xiàn)的問題解決分析
這篇文章主要為大家介紹了更換GORM默認(rèn)SQLite驅(qū)動出現(xiàn)的問題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01GoLang調(diào)用鏈可視化go-callvis使用介紹
與鏈路追蹤(Tracing)不同,Tracing關(guān)注復(fù)雜的分布式環(huán)境中各個服務(wù)節(jié)點間的調(diào)用關(guān)系,主要用于服務(wù)治理。而我們本次探索的代碼調(diào)用鏈路則是代碼方法級別的調(diào)用關(guān)系,主要用于代碼設(shè)計2023-02-02go-zero熔斷機(jī)制組件Breaker接口定義使用解析
這篇文章主要為大家介紹了go-zero熔斷機(jī)制組件Breaker接口定義使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05