写点什么

Golang 动态高效 JSON 解析技巧

作者:俞凡
  • 2024-02-24
    上海
  • 本文字数:4703 字

    阅读完需:约 15 分钟

JSON 如今广泛用于配置和通信协议,但由于其定义的灵活性,很容易传递错误数据。本文介绍了如何使用 mapstructure 工具实现动态灵活的 JSON 数据解析,在牺牲一定性能的前提下,有效提升开发效率和容错能力。原文: Efficient JSON Data Handling: Dynamic Parsing Tips in Golang


打造无缝 Golang 体验,探索动态 JSON 解析技术,实现最佳开发实践。


Cristian Palmer @Unsplash


在 Golang 开发领域,经常需要解析 JSON 数据。然而,如果值的类型不确定,是否有优雅的解决方案?


例如,当 JSON 字符串为 { "age":1 },而相应的结构体定义为字符串时,解析就会报错。


除了为结构体定义反序列化方法外,还有其他解决方案吗?今天,我将介绍另一种解决这一难题的方法。


Mapstructure 主要用于将任意 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"}}
复制代码


这种方法可能是最常用的,可以毫不费力地将 map[string]interface{} 映射到我们定义的结构。


在这里,我们并没有为每个字段指定标签,而是让 mapstructure 自动处理映射。


如果输入是 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 包含了 SchoolAddress 的嵌入式结构,并通过使用 squash 标签实现了扁平化效果。

# 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{} 之间的差异。相同部分可以正确映射到相应的字段,而不同的部分则使用 UnusedUnset 来表示。


  • Unused: map 中存在但结构中没有的字段。

  • Unset: 结构中存在但 map 中没有的字段。

# 4.避免空值映射

这里的用法类似于内置 json 软件包,利用 omitempty 标记来处理空值的映射。


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"}}
复制代码


从代码中可以看出,Other 字段被标记为 remain,意味着输入中任何不能正确映射的字段都将被放在 Other 字段中。


输出结果显示,emailgender 已被正确的放入 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_nameperson_age 分别映射为 NameAge,从而在不改变结构的情况下实现了正确的解析。

# 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{}}
复制代码


从代码中可以看出,输入的 nameage 类型与 Person 结构中的 NameAge 类型不匹配。


email 字段尤其不走寻常路,一个是字符串数组,另一个是 map。


通过自定义 DecoderConfig 并将 WeaklyTypedInput 设置为 truemapstructure 可以轻松解决此类弱类型解析问题。


不过,需要注意的是,并非所有问题都能得到解决,源代码也存在一定的局限性:


//   - 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.错误处理

Mapstructure 提供了用户非常方便使用的错误信息。


让我们看看它在遇到错误时是如何进行提示的。


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 纳入项目时,必须全面考虑产品逻辑和使用场景。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 19 分钟前阅读数: 5
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
Golang动态高效JSON解析技巧_golang_俞凡_InfoQ写作社区