在Golang开发领域,经常会出现解析JSON的需求。然而,当值的类型变得不确定时,我们是否有一个优雅的解决方案可供使用?
例如,当 JSON 字符串为 { "age": 1 } 且对应的结构体定义为字符串时,解析将导致错误。 除了为结构体定义反序列化方法之外,还有其他解决方案吗?
今天,我将介绍另一种方法来应对这一挑战。
Mapstruct 主要用于促进将任意 JSON 数据解码为 Go 的结构。在处理 JSON 数据中的动态或不确定类型时,它是一个强大的工具,提供了超越严格结构定义约束的灵活解决方案。
从本质上讲,它擅长解析具有可能无法完全理解的底层结构的数据流,并将它们映射到我们定义的结构中。
现在通过几个例子来探讨如何使用mapstructure。
type Person struct { Name string Age int Emails []string Extra map[string]string}
func normalDecode() { input := map[string]interface{}{ "name": "Foo", "age": 21, "emails": []string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, "extra": map[string]string{ "twitter": "Foo", }, } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v\n", result)}
结果:
main.Person{Name:"Foo", Age:21, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Foo"}}"Foo", Age:21, Emails:[]string{"one@gmail.com", "two@gmail.com", "three@gmail.com"}, Extra:map[string]string{"twitter":"Foo"}}
这种方法可能是最常用的,可以轻松地将 map[string]interface{} 映射到我们定义的结构。
这里,我们没有为每个字段指定标签,让mapstruct自动处理映射。
如果我们的输入是 JSON 字符串,我们首先将其解析为 map[string]interface{},然后将其映射到我们的结构中。
func jsonDecode() { var jsonStr = `{ "name": "Foo", "age": 21, "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:"Foo", Age:21, Gender:"male"}
MapStructure 使我们能够压缩多个嵌入结构并使用 squash 标签来处理它们。
type School struct { Name string}
type Address struct { City string}type Person struct { School `mapstructure:",squash"` Address `mapstructure:",squash"` Email string}func embeddedStructDecode() { input := map[string]interface{}{ "Name": "A1", "City": "B1", "Email": "C1", } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%s %s, %s\n", result.Name, result.City, result.Email)}
结果:
A1, B1, C1
在此示例中,Person 合并了 School 和 Address 的嵌入式结构,并通过使用挤压标签,实现了扁平化效果。
type Person struct { Name string Age int Gender string}
func metadataDecode() { input := map[string]interface{}{ "name": "A1", "age": 1, "email": "B1", } 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:"A1", Age:1, Gender:""}, keys: []string{"Name", "Age"}, Unused keys: []string{"email"}, Unset keys: []string{"Gender"}
从这个例子中,我们可以观察到使用元数据允许我们跟踪结构和map[string]interface{}之间的差异。相同的部分正确映射到相应的字段,而差异则使用 Unused 和 Unset 来表达。
Unused:map存在但结构中不存在的字段。
Unset:结构中存在但map中不存在的字段。
这里的用法类似于内置 json 包使用的方法,利用 omitempty 标签来解决 null 值的映射。
type School struct { Name string}
type Address struct { City string}type Person struct { *School `mapstructure:",omitempty"` *Address `mapstructure:",omitempty"` Age int Email string}func omitemptyDecode() { result := &map[string]interface{}{} input := Person{Email: "C1"} err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%+v\n", result)}
结果:
&map[Age:0 Email:C1]
在这里,我们观察到 *School 和 *Address 都被标记为 omitempty,这在解析过程中忽略了空值。
另一方面,Age 没有使用 omitempty 标记,并且由于输入中没有对应的值,因此解析时使用相应类型的零值,其中 int 的零值是 0。
type Person struct { Name string Age int Other map[string]interface{} `mapstructure:",remain"`}
func remainDataDecode() { input := map[string]interface{}{ "name": "A1", "age": 1, "email": "B1", "gender": "C1", } var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v\n", result)}
结果:
main.Person{Name:"A1", Age:1, Other:map[string]interface {}{"email":"B1", "gender":"C1"}}
从代码中可以明显看出,“其他”字段标记为“remain”,表示输入中未正确映射的任何字段都将放置在“other”中。
输出显示电子邮件和性别已正确放置在“other”中。
type Person struct { Name string `mapstructure:"person_name"` Age int `mapstructure:"person_age"`}func tagDecode() { input := map[string]interface{}{ "person_name": "A1", "person_age": 1, }
var result Person err := mapstructure.Decode(input, &result) if err != nil { panic(err) } fmt.Printf("%#v\n", result)}
结果:
main.Person{Name:"A1", Age:1}
在Person结构体中,我们将person_name和person_age分别映射到Name和Age,在不改变结构体的情况下实现了正确的解析。
type Person struct { Name string Age int Emails []string}func weaklyTypedInputDecode() { input := map[string]interface{}{ "name": 123, // number => string "age": "11", // 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:11, Emails:[]string{}}
从代码中可以明显看出,输入中的姓名、年龄类型与Person结构中的姓名、年龄类型不匹配。
电子邮件字段尤其非常规,在一种情况下是字符串数组,在另一种情况下是映射。
通过自定义 DecoderConfig 并将 WeaklyTypedInput 设置为 true,mapstruct 可以轻松帮助解决此类弱类型解析问题。
然而,需要注意的是,并非所有问题都可以解决,并且源代码揭示了某些局限性
// - 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.
Mapstruct 提供了非常用户友好的错误消息。 看一下它在遇到错误时是如何进行提示的。
type Person struct { Name string Age int Emails []string Extra map[string]string}
func decodeErrorHandle() { 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'
这里的错误消息告诉我们每个字段以及字段中的值应如何表示。这些错误提示可以指导我们高效解决问题。
上面的例子展示了mapstructure有效解决现实问题、提供实用解决方案和节省开发工作量的力量。
然而,从源代码的角度来看,很明显该库广泛使用了反射,这可能会在某些特殊场景中引入性能问题。
mapstructure因此,开发人员在融入项目时必须充分考虑产品逻辑和用例。