• golang mapstructure库实践


    简介

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

    快速使用

    本文代码采用 Go Modules。

    首先创建目录并初始化:

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

    下载mapstructure库:

    $ go get github.com/mitchellh/mapstructure

    使用:

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

    运行结果:

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

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

    内嵌结构

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

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

    为了正确解码,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. type Friend1 struct {
    5. Person
    6. }
    7. type Friend2 struct {
    8. Person `mapstructure:",squash"`
    9. }
    10. func main() {
    11. datas := []string{`
    12. {
    13. "type": "friend1",
    14. "person": {
    15. "name":"dj"
    16. }
    17. }
    18. `,
    19. `
    20. {
    21. "type": "friend2",
    22. "name": "dj2"
    23. }
    24. `,
    25. }
    26. for _, data := range datas {
    27. var m map[string]interface{}
    28. err := json.Unmarshal([]byte(data), &m)
    29. if err != nil {
    30. log.Fatal(err)
    31. }
    32. switch m["type"].(string) {
    33. case "friend1":
    34. var f1 Friend1
    35. mapstructure.Decode(m, &f1)
    36. fmt.Println("friend1", f1)
    37. case "friend2":
    38. var f2 Friend2
    39. mapstructure.Decode(m, &f2)
    40. fmt.Println("friend2", f2)
    41. }
    42. }
    43. }

    注意对比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. func main() {
    8. data := `
    9. {
    10. "name": "dj",
    11. "age":18,
    12. "job":"programmer",
    13. "height":"1.8m",
    14. "handsome": true
    15. }
    16. `
    17. var m map[string]interface{}
    18. err := json.Unmarshal([]byte(data), &m)
    19. if err != nil {
    20. log.Fatal(err)
    21. }
    22. var p Person
    23. mapstructure.Decode(m, &p)
    24. fmt.Println("other", p.Other)
    25. }

    上面代码中,我们为结构体定义了一个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. func main() {
    7. p := &Person{
    8. Name: "dj",
    9. Age: 18,
    10. }
    11. var m map[string]interface{}
    12. mapstructure.Decode(p, &m)
    13. data, _ := json.Marshal(m)
    14. fmt.Println(string(data))
    15. }

    上面代码中,我们为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. func main() {
    6. m := map[string]interface{}{
    7. "name": "dj",
    8. "age": 18,
    9. "job": "programmer",
    10. }
    11. var p Person
    12. var metadata mapstructure.Metadata
    13. mapstructure.DecodeMetadata(m, &p, &metadata)
    14. fmt.Printf("keys:%#v unused:%#v\n", metadata.Keys, metadata.Unused)
    15. }

    先定义一个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. func main() {
    7. m := map[string]interface{}{
    8. "name": 123,
    9. "age": "bad value",
    10. "emails": []int{1, 2, 3},
    11. }
    12. var p Person
    13. err := mapstructure.Decode(m, &p)
    14. if err != nil {
    15. fmt.Println(err.Error())
    16. }
    17. }

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

    1. $ go run main.go
    2. 5 error(s) decoding:
    3. * 'Age' expected type 'int', got unconvertible type 'string'
    4. * 'Emails[0]' expected type 'string', got unconvertible type 'int'
    5. * 'Emails[1]' expected type 'string', got unconvertible type 'int'
    6. * 'Emails[2]' expected type 'string', got unconvertible type 'int'
    7. * '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. func main() {
    7. m := map[string]interface{}{
    8. "name": 123,
    9. "age": "18",
    10. "emails": []int{1, 2, 3},
    11. }
    12. var p Person
    13. err := mapstructure.WeakDecode(m, &p)
    14. if err == nil {
    15. fmt.Println("person:", p)
    16. } else {
    17. fmt.Println(err.Error())
    18. }
    19. }

    虽然键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. func main() {
    6. m := map[string]interface{}{
    7. "name": 123,
    8. "age": "18",
    9. "job": "programmer",
    10. }
    11. var p Person
    12. var metadata mapstructure.Metadata
    13. decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
    14. WeaklyTypedInput: true,
    15. Result: &p,
    16. Metadata: &metadata,
    17. })
    18. if err != nil {
    19. log.Fatal(err)
    20. }
    21. err = decoder.Decode(m)
    22. if err == nil {
    23. fmt.Println("person:", p)
    24. fmt.Printf("keys:%#v, unused:%#v\n", metadata.Keys, metadata.Unused)
    25. } else {
    26. fmt.Println(err.Error())
    27. }
    28. }

    这里用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. decoder, err := NewDecoder(config)
    9. if err != nil {
    10. return err
    11. }
    12. return decoder.Decode(input)
    13. }

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

    总结

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

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

  • 相关阅读:
    使用 Footprint Analytics, 快速搭建区块链数据应用
    优雅的自定义 ThreadPoolExecutor 线程池
    浅浅的 使用网络调试助手和Mosquitto 分析MQTT协议数据包内容
    探秘公有IP地址与私有IP地址的区别及其在路由控制中的作用
    鸿蒙开发(五)鸿蒙UI开发概览
    dup dup2 不仅仅是int那么简单
    Git--Part3--远程操作 & 配置 & 标签管理
    CI/CD——Jenkins自动构建maven项目
    分布式存储技术解读系列之三:Swift | 架构进阶
    C++ Qt开发:QItemDelegate自定义代理组件
  • 原文地址:https://blog.csdn.net/qq_32421489/article/details/132814504