简介

mapstructure用于将通用的map[string]interface{}解码到对应的 Go 构造体中,或者执行相同的操作。很多时候,解析来自多种源头的数据流时,咱们个别当时并不知道他们对应的具体类型。只有读取到一些字段之后能力做出判断。这时,咱们能够先应用规范的encoding/json库将数据解码为map[string]interface{}类型,而后依据标识字段利用mapstructure库转为相应的 Go 构造体以便应用。

疾速应用

本文代码采纳 Go Modules。

首先创立目录并初始化:

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

下载mapstructure库:

$ go get github.com/mitchellh/mapstructure

应用:

package mainimport (  "encoding/json"  "fmt"  "log"  "github.com/mitchellh/mapstructure")type Person struct {  Name string  Age  int  Job  string}type Cat struct {  Name  string  Age   int  Breed string}func main() {  datas := []string{`    {       "type": "person",      "name":"dj",      "age":18,      "job": "programmer"    }  `,    `    {      "type": "cat",      "name": "kitty",      "age": 1,      "breed": "Ragdoll"    }  `,  }  for _, data := range datas {    var m map[string]interface{}    err := json.Unmarshal([]byte(data), &m)    if err != nil {      log.Fatal(err)    }    switch m["type"].(string) {    case "person":      var p Person      mapstructure.Decode(m, &p)      fmt.Println("person", p)    case "cat":      var cat Cat      mapstructure.Decode(m, &cat)      fmt.Println("cat", cat)    }  }}

运行后果:

$ go run main.goperson {dj 18 programmer}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是大小写不敏感的!

type Person struct {  Name string}

当然,咱们也能够指定映射的字段名。为了做到这一点,咱们须要为字段设置mapstructure标签。例如上面应用username代替上例中的name

type Person struct {  Name string `mapstructure:"username"`}

看示例:

type Person struct {  Name string `mapstructure:"username"`  Age  int  Job  string}type Cat struct {  Name  string  Age   int  Breed string}func main() {  datas := []string{`    {       "type": "person",      "username":"dj",      "age":18,      "job": "programmer"    }  `,    `    {      "type": "cat",      "name": "kitty",      "Age": 1,      "breed": "Ragdoll"    }  `,    `    {      "type": "cat",      "Name": "rooooose",      "age": 2,      "breed": "shorthair"    }  `,  }  for _, data := range datas {    var m map[string]interface{}    err := json.Unmarshal([]byte(data), &m)    if err != nil {      log.Fatal(err)    }    switch m["type"].(string) {    case "person":      var p Person      mapstructure.Decode(m, &p)      fmt.Println("person", p)    case "cat":      var cat Cat      mapstructure.Decode(m, &cat)      fmt.Println("cat", cat)    }  }}

下面代码中,咱们应用标签mapstructure:"username"PersonName字段映射为username,在 JSON 串中咱们须要设置username能力正确解析。另外,留神到,咱们将第二个 JSON 串中的Age和第三个 JSON 串中的Name首字母大写了,然而并没有影响解码后果。mapstructure解决字段映射是大小写不敏感的。

内嵌构造

构造体能够任意嵌套,嵌套的构造被认为是领有该构造体名字的另一个字段。例如,上面两种Friend的定义形式对于mapstructure是一样的:

type Person struct {  Name string}// 形式一type Friend struct {  Person}// 形式二type Friend struct {  Person Person}

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

map[string]interface{} {  "person": map[string]interface{}{"name": "dj"},}

咱们也能够设置mapstructure:",squash"将该构造体的字段提到父构造中:

type Friend struct {  Person `mapstructure:",squash"`}

这样只须要这样的 JSON 串,有效嵌套person键:

map[string]interface{}{  "name": "dj",}

看示例:

type Person struct {  Name string}type Friend1 struct {  Person}type Friend2 struct {  Person `mapstructure:",squash"`}func main() {  datas := []string{`    {       "type": "friend1",      "person": {        "name":"dj"      }    }  `,    `    {      "type": "friend2",      "name": "dj2"    }  `,  }  for _, data := range datas {    var m map[string]interface{}    err := json.Unmarshal([]byte(data), &m)    if err != nil {      log.Fatal(err)    }    switch m["type"].(string) {    case "friend1":      var f1 Friend1      mapstructure.Decode(m, &f1)      fmt.Println("friend1", f1)    case "friend2":      var f2 Friend2      mapstructure.Decode(m, &f2)      fmt.Println("friend2", f2)    }  }}

留神比照Friend1Friend2应用的 JSON 串的不同。

另外须要留神一点,如果父构造体中有同名的字段,那么mapstructure会将JSON 中对应的值同时设置到这两个字段中,即这两个字段有雷同的值。

未映射的值

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

咱们能够在构造体中定义一个字段,为其设置mapstructure:",remain"标签。这样未映射的值就会增加到这个字段中。留神,这个字段的类型只能为map[string]interface{}map[interface{}]interface{}

看示例:

type Person struct {  Name  string  Age   int  Job   string  Other map[string]interface{} `mapstructure:",remain"`}func main() {  data := `    {       "name": "dj",      "age":18,      "job":"programmer",      "height":"1.8m",      "handsome": true    }  `  var m map[string]interface{}  err := json.Unmarshal([]byte(data), &m)  if err != nil {    log.Fatal(err)  }  var p Person  mapstructure.Decode(m, &p)  fmt.Println("other", p.Other)}

下面代码中,咱们为构造体定义了一个Other字段,用于保留未映射的键值。输入后果:

other map[handsome:true height:1.8m]

逆向转换

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

type Person struct {  Name string  Age  int  Job  string `mapstructure:",omitempty"`}func main() {  p := &Person{    Name: "dj",    Age:  18,  }  var m map[string]interface{}  mapstructure.Decode(p, &m)  data, _ := json.Marshal(m)  fmt.Println(string(data))}

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

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

Metadata

解码时会产生一些有用的信息,mapstructure能够应用Metadata收集这些信息。Metadata构造如下:

// mapstructure.gotype Metadata struct {  Keys   []string  Unused []string}

Metadata只有两个导出字段:

  • Keys:解码胜利的键名;
  • Unused:在源数据中存在,然而指标构造中不存在的键名。

为了收集这些数据,咱们须要应用DecodeMetadata来代替Decode办法:

type Person struct {  Name string  Age  int}func main() {  m := map[string]interface{}{    "name": "dj",    "age":  18,    "job":  "programmer",  }  var p Person  var metadata mapstructure.Metadata  mapstructure.DecodeMetadata(m, &p, &metadata)  fmt.Printf("keys:%#v unused:%#v\n", metadata.Keys, metadata.Unused)}

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

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

错误处理

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

type Person struct {  Name   string  Age    int  Emails []string}func main() {  m := map[string]interface{}{    "name":   123,    "age":    "bad value",    "emails": []int{1, 2, 3},  }  var p Person  err := mapstructure.Decode(m, &p)  if err != nil {    fmt.Println(err.Error())  }}

下面代码中,构造体中Person中字段Namestring类型,但输出中nameint类型;字段Ageint类型,但输出中agestring类型;字段Emails[]string类型,但输出中emails[]int类型。故Decode返回谬误。运行后果:

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

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

弱类型输出

有时候,咱们并不想对构造体字段类型和map[string]interface{}的对应键值做强类型统一的校验。这时能够应用WeakDecode/WeakDecodeMetadata办法,它们会尝试做类型转换:

type Person struct {  Name   string  Age    int  Emails []string}func main() {  m := map[string]interface{}{    "name":   123,    "age":    "18",    "emails": []int{1, 2, 3},  }  var p Person  err := mapstructure.WeakDecode(m, &p)  if err == nil {    fmt.Println("person:", p)  } else {    fmt.Println(err.Error())  }}

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

解码器

除了下面介绍的办法外,mapstructure还提供了更灵便的解码器(Decoder)。能够通过配置DecoderConfig实现下面介绍的任何性能:

// mapstructure.gotype DecoderConfig struct {    ErrorUnused       bool    ZeroFields        bool    WeaklyTypedInput  bool    Metadata          *Metadata    Result            interface{}    TagName           string}

各个字段含意如下:

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

看示例:

type Person struct {  Name string  Age  int}func main() {  m := map[string]interface{}{    "name": 123,    "age":  "18",    "job":  "programmer",  }  var p Person  var metadata mapstructure.Metadata  decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{    WeaklyTypedInput: true,    Result:           &p,    Metadata:         &metadata,  })  if err != nil {    log.Fatal(err)  }  err = decoder.Decode(m)  if err == nil {    fmt.Println("person:", p)    fmt.Printf("keys:%#v, unused:%#v\n", metadata.Keys, metadata.Unused)  } else {    fmt.Println(err.Error())  }}

这里用Decoder的形式实现了后面弱类型输出大节中的示例代码。实际上WeakDecode外部就是通过这种形式实现的,上面是WeakDecode的源码:

// mapstructure.gofunc WeakDecode(input, output interface{}) error {  config := &DecoderConfig{    Metadata:         nil,    Result:           output,    WeaklyTypedInput: true,  }  decoder, err := NewDecoder(config)  if err != nil {    return err  }  return decoder.Decode(input)}

再实际上,Decode/DecodeMetadata/WeakDecodeMetadata外部都是先设置DecoderConfig的对应字段,而后创立Decoder对象,最初调用其Decode办法实现的。

总结

mapstructure实现优雅,功能丰富,代码构造清晰,十分举荐一看!

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

参考

  1. mapstructure GitHub:https://github.com/mitchellh/mapstructure
  2. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~