使用 mapstructure 解析 json

TimLiuDream · 2023-12-27 23:55:36 · 869 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2023-12-27 23:55:36 的主题,其中的信息可能已经有所发展或是发生改变。

关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!

背景

前几天群里的小伙伴问了一个这样的问题:

Snipaste_2023-12-26_22-07-48.png

其实质就是在面对 value 类型不确定的情况下,怎么解析这个 json?

我下意识就想到了 [mapstructure](https://github.com/mitchellh/mapstructure) 这个库,它可以帮助我们类似 PHP 那样去处理弱类型的结构。

介绍

先来介绍一下 mapstructure 这个库主要用来做什么的吧,官网是这么介绍的:

mapstructure 是一个 Go 库,用于将通用映射值解码为结构,反之亦然,同时提供有用的错误处理。

该库在解码数据流(JSON、Gob 等)中的值时最为有用,因为在读取部分数据之前,您并不十分清楚底层数据的结构。因此,您可以读取 map[string]interface{} 并使用此库将其解码为适当的本地 Go 底层结构。

简单来说,它擅长解析一些我们并不十分清楚底层数据结构的数据流到我们定义的结构体中。

下面我们通过几个例子来简单介绍一下 mapstructure 怎么使用。

例子

普通形式

func normalDecode() {
    type Person struct {
        Name   string
        Age    int
        Emails []string
        Extra  map[string]string
    }

    // 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
    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"}}

这个方式应该是我们最经常使用的,非常简单的将 map[string]interface{} 映射到我们的结构体中。

在这里,我们并没有指定每个 fieldtag,让 mapstructure 自动去映射。

如果我们的 input 是一个 json 字符串,那么我们需要将 json 字符串解析为 map[string]interface{} 之后,再将其映射到我们的结构体中。

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

嵌入式结构

mapstructure 允许我们压缩多个嵌入式结构,并通过 squash 标签进行处理。

func embeddedStructDecode() {
    // 使用 squash 标签允许压缩多个嵌入式结构。通过创建多种类型的复合结构并对其进行解码来演示此功能。
    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 里面有着 LocationFamily 的嵌入式结构体,通过 squash 标签进行压缩,从而达到平铺的作用。

元数据

func metadataDecode() {
    type Person struct {
        Name   string
        Age    int
        Gender string
    }

    // 此输入可以来自任何地方,但通常来自诸如解码 JSON 之类的东西,我们最初不太确定结构。
    input := map[string]interface{}{
        "name":  "Tim",
        "age":   31,
        "email": "one@gmail.com",
    }

    // 对于元数据,我们制作了一个更高级的 DecoderConfig,以便我们可以更细致地配置所使用的解码器。在这种情况下,我们只是告诉解码器我们想要跟踪元数据。
    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 可以记录我们结构体以及 map[string]interface{} 的差异,相同的部分会正确映射到对应的字段中,而差异则使用了 UnusedUnset 来表达。

  • Unused:map 中有着结构体所没有的字段。
  • Unset:结构体中有着 map 中所没有的字段。

避免空值的映射

这里的使用其实和内置的 json 库使用方式是一样的,都是借助 omitempty 标签来解决。

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 都被设置了 omitempty,所以在解析过程中会忽略掉空值。而 Age 没有设置,并且 input 中没有对应的 value,所以在解析中使用对应类型的零值来表达,而 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 字段被设置了 remain,这意味着 input 中没有正确映射的字段都会被放到 Other 中,从输出可以看到,emailgender 已经被正确的放到 Other 中了。

自定义标签

func tagDecode() {
    // 请注意,结构类型中定义的 mapstructure 标签可以指示将值映射到哪些字段。
    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 结构中,我们将 person_nameperson_age 分别映射到 NameAge 中,从而达到在不破坏结构的基础上,去正确的解析。

弱类型解析

正如前面所说,mapstructure 提供了类似 PHP 解析弱类型结构的方法。

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 中的 nameagePerson 结构体中的 NameAge 类型不一致,而 email 更是离谱,一个字符串数组,一个是 map

但是我们通过自定义 DecoderConfig,将 WeaklyTypedInput 设置成 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'

这里的错误提示会告诉我们每个字段,字段里的值应该需要怎么表达,我们可以通过这些错误提示,比较快的去修复问题。

总结

从上面这些例子看看到 mapstructure 的强大之处,很好的帮我们解决了实实在在的问题,也在节省我们的开发成本。

但是从源码来看,内部使用了大量的反射,这可能会对一些特殊场景带来性能隐患。所以大家在使用的时候,一定要充分考虑产品逻辑以及场景。

以下贴一小段删减过的源码:

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

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

869 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传