Go 每日一库之 mapstructure

简介

https://blog.csdn.net/qq_40530622/article/details/119408623

https://blog.csdn.net/ma_jiang/article/details/120720840

mapstructure用于将通用的map[string]interface{}解码到对应的 Go 结构体中,或者执行相反的操作。很多时候,解析来自多种源头的数据流时,我们一般事先并不知道他们对应的具体类型。只有读取到一些字段之后才能做出判断。这时,我们可以先使用标准的encoding/json库将数据解码为map[string]interface{}类型,然后根据标识字段利用mapstructure库转为相应的 Go 结构体以便使用。

快速使用

本文代码采用 Go Modules。

首先创建目录并初始化:

  1.  
    $ mkdir mapstructure && cd mapstructure
  2.  
     
  3.  
    $ go mod init github.com/darjun/go-daily-lib/mapstructure

下载mapstructure库:

$ go get github.com/mitchellh/mapstructure

使用:

  1.  
    package main
  2.  
     
  3.  
    import (
  4.  
    "encoding/json"
  5.  
    "fmt"
  6.  
    "log"
  7.  
     
  8.  
    "github.com/mitchellh/mapstructure"
  9.  
    )
  10.  
     
  11.  
    type Person struct {
  12.  
    Name string
  13.  
    Age int
  14.  
    Job string
  15.  
    }
  16.  
     
  17.  
    type Cat struct {
  18.  
    Name string
  19.  
    Age int
  20.  
    Breed string
  21.  
    }
  22.  
     
  23.  
    func main() {
  24.  
    datas := []string{`
  25.  
    {
  26.  
    "type": "person",
  27.  
    "name":"dj",
  28.  
    "age":18,
  29.  
    "job": "programmer"
  30.  
    }
  31.  
    `,
  32.  
    `
  33.  
    {
  34.  
    "type": "cat",
  35.  
    "name": "kitty",
  36.  
    "age": 1,
  37.  
    "breed": "Ragdoll"
  38.  
    }
  39.  
    `,
  40.  
    }
  41.  
     
  42.  
    for _, data := range datas {
  43.  
    var m map[string]interface{}
  44.  
    err := json.Unmarshal([]byte(data), &m)
  45.  
    if err != nil {
  46.  
    log.Fatal(err)
  47.  
    }
  48.  
     
  49.  
    switch m["type"].(string) {
  50.  
    case "person":
  51.  
    var p Person
  52.  
    mapstructure.Decode(m, &p)
  53.  
    fmt.Println("person", p)
  54.  
     
  55.  
    case "cat":
  56.  
    var cat Cat
  57.  
    mapstructure.Decode(m, &cat)
  58.  
    fmt.Println("cat", cat)
  59.  
    }
  60.  
    }
  61.  
    }

运行结果:

  1.  
    $ go run main.go
  2.  
    person {dj 18 programmer}
  3.  
    cat {kitty 1 Ragdoll}

我们定义了两个结构体PersonCat,他们的字段有些许不同。现在,我们约定通信的 JSON 串中有一个type字段。当type的值为person时,该 JSON 串表示的是Person类型的数据。当type的值为cat时,该 JSON 串表示的是Cat类型的数据。

上面代码中,我们先用json.Unmarshal将字节流解码为map[string]interface{}类型。然后读取里面的type字段。根据type字段的值,再使用mapstructure.Decode将该 JSON 串分别解码为PersonCat类型的值,并输出。

实际上,Google Protobuf 通常也使用这种方式。在协议中添加消息 ID 或全限定消息名。接收方收到数据后,先读取协议 ID 或全限定消息名。然后调用 Protobuf 的解码方法将其解码为对应的Message结构。从这个角度来看,mapstructure也可以用于网络消息解码,如果你不考虑性能的话 。

字段标签

默认情况下,mapstructure使用结构体中字段的名称做这个映射,例如我们的结构体有一个Name字段,mapstructure解码时会在map[string]interface{}中查找键名name。注意,这里的name是大小写不敏感的!

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    }

当然,我们也可以指定映射的字段名。为了做到这一点,我们需要为字段设置mapstructure标签。例如下面使用username代替上例中的name

  1.  
    type Person struct {
  2.  
    Name string `mapstructure:"username"`
  3.  
    }

看示例:

  1.  
    type Person struct {
  2.  
    Name string `mapstructure:"username"`
  3.  
    Age int
  4.  
    Job string
  5.  
    }
  6.  
     
  7.  
    type Cat struct {
  8.  
    Name string
  9.  
    Age int
  10.  
    Breed string
  11.  
    }
  12.  
     
  13.  
    func main() {
  14.  
    datas := []string{`
  15.  
    {
  16.  
    "type": "person",
  17.  
    "username":"dj",
  18.  
    "age":18,
  19.  
    "job": "programmer"
  20.  
    }
  21.  
    `,
  22.  
    `
  23.  
    {
  24.  
    "type": "cat",
  25.  
    "name": "kitty",
  26.  
    "Age": 1,
  27.  
    "breed": "Ragdoll"
  28.  
    }
  29.  
    `,
  30.  
    `
  31.  
    {
  32.  
    "type": "cat",
  33.  
    "Name": "rooooose",
  34.  
    "age": 2,
  35.  
    "breed": "shorthair"
  36.  
    }
  37.  
    `,
  38.  
    }
  39.  
     
  40.  
    for _, data := range datas {
  41.  
    var m map[string]interface{}
  42.  
    err := json.Unmarshal([]byte(data), &m)
  43.  
    if err != nil {
  44.  
    log.Fatal(err)
  45.  
    }
  46.  
     
  47.  
    switch m["type"].(string) {
  48.  
    case "person":
  49.  
    var p Person
  50.  
    mapstructure.Decode(m, &p)
  51.  
    fmt.Println("person", p)
  52.  
     
  53.  
    case "cat":
  54.  
    var cat Cat
  55.  
    mapstructure.Decode(m, &cat)
  56.  
    fmt.Println("cat", cat)
  57.  
    }
  58.  
    }
  59.  
    }

上面代码中,我们使用标签mapstructure:"username"PersonName字段映射为username,在 JSON 串中我们需要设置username才能正确解析。另外,注意到,我们将第二个 JSON 串中的Age和第三个 JSON 串中的Name首字母大写了,但是并没有影响解码结果。mapstructure处理字段映射是大小写不敏感的。

内嵌结构

结构体可以任意嵌套,嵌套的结构被认为是拥有该结构体名字的另一个字段。例如,下面两种Friend的定义方式对于mapstructure是一样的:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    }
  4.  
     
  5.  
    // 方式一
  6.  
    type Friend struct {
  7.  
    Person
  8.  
    }
  9.  
     
  10.  
    // 方式二
  11.  
    type Friend struct {
  12.  
    Person Person
  13.  
    }

为了正确解码,Person结构的数据要在person键下:

  1.  
    map[string]interface{} {
  2.  
    "person": map[string]interface{}{"name": "dj"},
  3.  
    }

我们也可以设置mapstructure:",squash"将该结构体的字段提到父结构中:

  1.  
    type Friend struct {
  2.  
    Person `mapstructure:",squash"`
  3.  
    }

这样只需要这样的 JSON 串,无效嵌套person键:

  1.  
    map[string]interface{}{
  2.  
    "name": "dj",
  3.  
    }

看示例:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    }
  4.  
     
  5.  
    type Friend1 struct {
  6.  
    Person
  7.  
    }
  8.  
     
  9.  
    type Friend2 struct {
  10.  
    Person `mapstructure:",squash"`
  11.  
    }
  12.  
     
  13.  
    func main() {
  14.  
    datas := []string{`
  15.  
    {
  16.  
    "type": "friend1",
  17.  
    "person": {
  18.  
    "name":"dj"
  19.  
    }
  20.  
    }
  21.  
    `,
  22.  
    `
  23.  
    {
  24.  
    "type": "friend2",
  25.  
    "name": "dj2"
  26.  
    }
  27.  
    `,
  28.  
    }
  29.  
     
  30.  
    for _, data := range datas {
  31.  
    var m map[string]interface{}
  32.  
    err := json.Unmarshal([]byte(data), &m)
  33.  
    if err != nil {
  34.  
    log.Fatal(err)
  35.  
    }
  36.  
     
  37.  
    switch m["type"].(string) {
  38.  
    case "friend1":
  39.  
    var f1 Friend1
  40.  
    mapstructure.Decode(m, &f1)
  41.  
    fmt.Println("friend1", f1)
  42.  
     
  43.  
    case "friend2":
  44.  
    var f2 Friend2
  45.  
    mapstructure.Decode(m, &f2)
  46.  
    fmt.Println("friend2", f2)
  47.  
    }
  48.  
    }
  49.  
    }

注意对比Friend1Friend2使用的 JSON 串的不同。

另外需要注意一点,如果父结构体中有同名的字段,那么mapstructure会将JSON 中对应的值同时设置到这两个字段中,即这两个字段有相同的值。

未映射的值

如果源数据中有未映射的值(即结构体中无对应的字段),mapstructure默认会忽略它。

我们可以在结构体中定义一个字段,为其设置mapstructure:",remain"标签。这样未映射的值就会添加到这个字段中。注意,这个字段的类型只能为map[string]interface{}map[interface{}]interface{}

看示例:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    Age int
  4.  
    Job string
  5.  
    Other map[string]interface{} `mapstructure:",remain"`
  6.  
    }
  7.  
     
  8.  
    func main() {
  9.  
    data := `
  10.  
    {
  11.  
    "name": "dj",
  12.  
    "age":18,
  13.  
    "job":"programmer",
  14.  
    "height":"1.8m",
  15.  
    "handsome": true
  16.  
    }
  17.  
    `
  18.  
     
  19.  
    var m map[string]interface{}
  20.  
    err := json.Unmarshal([]byte(data), &m)
  21.  
    if err != nil {
  22.  
    log.Fatal(err)
  23.  
    }
  24.  
     
  25.  
    var p Person
  26.  
    mapstructure.Decode(m, &p)
  27.  
    fmt.Println("other", p.Other)
  28.  
    }

上面代码中,我们为结构体定义了一个Other字段,用于保存未映射的键值。输出结果:

other map[handsome:true height:1.8m]

逆向转换

前面我们都是将map[string]interface{}解码到 Go 结构体中。mapstructure当然也可以将 Go 结构体反向解码为map[string]interface{}。在反向解码时,我们可以为某些字段设置mapstructure:",omitempty"。这样当这些字段为默认值时,就不会出现在结构的map[string]interface{}中:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    Age int
  4.  
    Job string `mapstructure:",omitempty"`
  5.  
    }
  6.  
     
  7.  
    func main() {
  8.  
    p := &Person{
  9.  
    Name: "dj",
  10.  
    Age: 18,
  11.  
    }
  12.  
     
  13.  
    var m map[string]interface{}
  14.  
    mapstructure.Decode(p, &m)
  15.  
     
  16.  
    data, _ := json.Marshal(m)
  17.  
    fmt.Println(string(data))
  18.  
    }

上面代码中,我们为Job字段设置了mapstructure:",omitempty",且对象pJob字段未设置。运行结果:

  1.  
    $ go run main.go
  2.  
    {"Age":18,"Name":"dj"}

Metadata

解码时会产生一些有用的信息,mapstructure可以使用Metadata收集这些信息。Metadata结构如下:

  1.  
    // mapstructure.go
  2.  
    type Metadata struct {
  3.  
    Keys []string
  4.  
    Unused []string
  5.  
    }

Metadata只有两个导出字段:

  • Keys:解码成功的键名;
  • Unused:在源数据中存在,但是目标结构中不存在的键名。

为了收集这些数据,我们需要使用DecodeMetadata来代替Decode方法:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    Age int
  4.  
    }
  5.  
     
  6.  
    func main() {
  7.  
    m := map[string]interface{}{
  8.  
    "name": "dj",
  9.  
    "age": 18,
  10.  
    "job": "programmer",
  11.  
    }
  12.  
     
  13.  
    var p Person
  14.  
    var metadata mapstructure.Metadata
  15.  
    mapstructure.DecodeMetadata(m, &p, &metadata)
  16.  
     
  17.  
    fmt.Printf("keys:%#v unused:%#v\n", metadata.Keys, metadata.Unused)
  18.  
    }

先定义一个Metadata结构,传入DecodeMetadata收集解码的信息。运行结果:

  1.  
    $ go run main.go
  2.  
    keys:[]string{"Name", "Age"} unused:[]string{"job"}

错误处理

mapstructure执行转换的过程中不可避免地会产生错误,例如 JSON 中某个键的类型与对应 Go 结构体中的字段类型不一致。Decode/DecodeMetadata会返回这些错误:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    Age int
  4.  
    Emails []string
  5.  
    }
  6.  
     
  7.  
    func main() {
  8.  
    m := map[string]interface{}{
  9.  
    "name": 123,
  10.  
    "age": "bad value",
  11.  
    "emails": []int{1, 2, 3},
  12.  
    }
  13.  
     
  14.  
    var p Person
  15.  
    err := mapstructure.Decode(m, &p)
  16.  
    if err != nil {
  17.  
    fmt.Println(err.Error())
  18.  
    }
  19.  
    }

上面代码中,结构体中Person中字段Namestring类型,但输入中nameint类型;字段Ageint类型,但输入中agestring类型;字段Emails[]string类型,但输入中emails[]int类型。故Decode返回错误。运行结果:

  1.  
    $ go run main.go
  2.  
    5 error(s) decoding:
  3.  
     
  4.  
    * 'Age' expected type 'int', got unconvertible type 'string'
  5.  
    * 'Emails[0]' expected type 'string', got unconvertible type 'int'
  6.  
    * 'Emails[1]' expected type 'string', got unconvertible type 'int'
  7.  
    * 'Emails[2]' expected type 'string', got unconvertible type 'int'
  8.  
    * 'Name' expected type 'string', got unconvertible type 'int'

从错误信息中很容易看出哪里出错了。

弱类型输入

有时候,我们并不想对结构体字段类型和map[string]interface{}的对应键值做强类型一致的校验。这时可以使用WeakDecode/WeakDecodeMetadata方法,它们会尝试做类型转换:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    Age int
  4.  
    Emails []string
  5.  
    }
  6.  
     
  7.  
    func main() {
  8.  
    m := map[string]interface{}{
  9.  
    "name": 123,
  10.  
    "age": "18",
  11.  
    "emails": []int{1, 2, 3},
  12.  
    }
  13.  
     
  14.  
    var p Person
  15.  
    err := mapstructure.WeakDecode(m, &p)
  16.  
    if err == nil {
  17.  
    fmt.Println("person:", p)
  18.  
    } else {
  19.  
    fmt.Println(err.Error())
  20.  
    }
  21.  
    }

虽然键name对应的值123int类型,但是在WeakDecode中会将其转换为string类型以匹配Person.Name字段的类型。同样的,age的值"18"string类型,在WeakDecode中会将其转换为int类型以匹配Person.Age字段的类型。 需要注意一点,如果类型转换失败了,WeakDecode同样会返回错误。例如将上例中的age设置为"bad value",它就不能转为int类型,故而返回错误。

解码器

除了上面介绍的方法外,mapstructure还提供了更灵活的解码器(Decoder)。可以通过配置DecoderConfig实现上面介绍的任何功能:

  1.  
    // mapstructure.go
  2.  
    type DecoderConfig struct {
  3.  
    ErrorUnused bool
  4.  
    ZeroFields bool
  5.  
    WeaklyTypedInput bool
  6.  
    Metadata *Metadata
  7.  
    Result interface{}
  8.  
    TagName string
  9.  
    }

各个字段含义如下:

  • ErrorUnused:为true时,如果输入中的键值没有与之对应的字段就返回错误;
  • ZeroFields:为true时,在Decode前清空目标map。为false时,则执行的是map的合并。用在structmap的转换中;
  • WeaklyTypedInput:实现WeakDecode/WeakDecodeMetadata的功能;
  • Metadata:不为nil时,收集Metadata数据;
  • Result:为结果对象,在mapstruct的转换中,Resultstruct类型。在structmap的转换中,Resultmap类型;
  • TagName:默认使用mapstructure作为结构体的标签名,可以通过该字段设置。

看示例:

  1.  
    type Person struct {
  2.  
    Name string
  3.  
    Age int
  4.  
    }
  5.  
     
  6.  
    func main() {
  7.  
    m := map[string]interface{}{
  8.  
    "name": 123,
  9.  
    "age": "18",
  10.  
    "job": "programmer",
  11.  
    }
  12.  
     
  13.  
    var p Person
  14.  
    var metadata mapstructure.Metadata
  15.  
     
  16.  
    decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
  17.  
    WeaklyTypedInput: true,
  18.  
    Result: &p,
  19.  
    Metadata: &metadata,
  20.  
    })
  21.  
     
  22.  
    if err != nil {
  23.  
    log.Fatal(err)
  24.  
    }
  25.  
     
  26.  
    err = decoder.Decode(m)
  27.  
    if err == nil {
  28.  
    fmt.Println("person:", p)
  29.  
    fmt.Printf("keys:%#v, unused:%#v\n", metadata.Keys, metadata.Unused)
  30.  
    } else {
  31.  
    fmt.Println(err.Error())
  32.  
    }
  33.  
    }

这里用Decoder的方式实现了前面弱类型输入小节中的示例代码。实际上WeakDecode内部就是通过这种方式实现的,下面是WeakDecode的源码:

  1.  
    // mapstructure.go
  2.  
    func WeakDecode(input, output interface{}) error {
  3.  
    config := &DecoderConfig{
  4.  
    Metadata: nil,
  5.  
    Result: output,
  6.  
    WeaklyTypedInput: true,
  7.  
    }
  8.  
     
  9.  
    decoder, err := NewDecoder(config)
  10.  
    if err != nil {
  11.  
    return err
  12.  
    }
  13.  
     
  14.  
    return decoder.Decode(input)
  15.  
    }

再实际上,Decode/DecodeMetadata/WeakDecodeMetadata内部都是先设置DecoderConfig的对应字段,然后创建Decoder对象,最后调用其Decode方法实现的。

总结

mapstructure实现优雅,功能丰富,代码结构清晰,非常推荐一看!

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue

posted @ 2023-05-25 21:44  技术颜良  阅读(89)  评论(0编辑  收藏  举报