Golang 动态解析技巧:提升 JSON 数据处理效率

发表时间: 2024-01-24 11:00

在Golang开发领域,经常会出现解析JSON的需求。然而,当值的类型变得不确定时,我们是否有一个优雅的解决方案可供使用?

例如,当 JSON 字符串为 { "age": 1 } 且对应的结构体定义为字符串时,解析将导致错误。 除了为结构体定义反序列化方法之外,还有其他解决方案吗?

今天,我将介绍另一种方法来应对这一挑战。

Mapstruct 主要用于促进将任意 JSON 数据解码为 Go 的结构。在处理 JSON 数据中的动态或不确定类型时,它是一个强大的工具,提供了超越严格结构定义约束的灵活解决方案。

从本质上讲,它擅长解析具有可能无法完全理解的底层结构的数据流,并将它们映射到我们定义的结构中。

现在通过几个例子来探讨如何使用mapstructure

1. 常规用法

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"}

2、 嵌入结构

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 的嵌入式结构,并通过使用挤压标签,实现了扁平化效果。

3、元数据

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中不存在的字段。

4、 避免映射空值

这里的用法类似于内置 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”中。

5、 自定义标签

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,在不改变结构体的情况下实现了正确的解析。

6、弱类型解析

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.

7、错误处理

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因此,开发人员在融入项目时必须充分考虑产品逻辑和用例。