Go 每日一庫之 mapstructure

簡介

mapstructure用於將通用的map[string]interface{}解碼到對應的 Go 結構體中,或者執行相反的操做。不少時候,解析來自多種源頭的數據流時,咱們通常事先並不知道他們對應的具體類型。只有讀取到一些字段以後才能作出判斷。這時,咱們能夠先使用標準的encoding/json庫將數據解碼爲map[string]interface{}類型,而後根據標識字段利用mapstructure庫轉爲相應的 Go 結構體以便使用。git

快速使用

本文代碼採用 Go Modules。github

首先建立目錄並初始化:golang

$ mkdir mapstructure && cd mapstructure

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

下載mapstructure庫:json

$ go get github.com/mitchellh/mapstructure

使用:微信

package main

import (
  "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.go
person {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也能夠用於網絡消息解碼,若是你不考慮性能的話😄。spa

字段標籤

默認狀況下,mapstructure使用結構體中字段的名稱作這個映射,例如咱們的結構體有一個Name字段,mapstructure解碼時會在map[string]interface{}中查找鍵名name。注意,這裏的name是大小寫不敏感的!code

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.go
type 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.go
type 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.go
func 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】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索