首頁 > 軟體

Golang中結構體對映mapstructure庫深入詳解

2023-01-04 14:00:11

在資料傳遞時,需要先編解碼;常用的方式是JSON編解碼(參見《golang之JSON處理》)。但有時卻需要讀取部分欄位後,才能知道具體型別,此時就可藉助mapstructure庫了。

mapstructure庫

mapstructure可方便地實現map[string]interface{}struct間的轉換;使用前,需要先匯入庫:

go get github.com/mitchellh/mapstructure

欄位標籤

預設情況下,mapstructure使用欄位的名稱做匹配對映(即在map中以欄位名為鍵值查詢欄位值);注意匹配時是忽略大小寫的。也可通過標籤來設定欄位對映名稱:

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

內嵌結構

go中結構體是可以任意巢狀的;巢狀後即認為擁有對應的欄位。但是,預設情況下mapstructure只處理當前結構定義的欄位,若要自動處理內嵌欄位需要新增標籤squash

type Student struct {
  Person `mapstructure:",squash"`
  Age int 
}

未對映欄位

若源資料中有未對映的值(即結構體中無對應的欄位),mapstructure預設會忽略它。可以在結構體中定義一個特殊欄位(型別為map[string]interface{},且標籤要設定為mapstructure:",remain"),來存放所有未能對映的欄位中。

type Student struct {
  Name  string
  Age   int
  Other map[string]interface{} `mapstructure:",remain"`
}

Metadata

mapstructure中可以使用Metadata收集一些解碼時會產生的有用資訊。

// mapstructure.go
type Metadata struct {
  Keys   []string  // 解碼成功的鍵
  Unused []string  // 源資料中存在,但目標結構中不存在的鍵
  Unset  []string  // 未設定的(源資料中缺失的)鍵
}

為了獲取這些資訊,需要使用DecodeMetadata來解碼:

  var metadata mapstructure.Metadata
  err := mapstructure.DecodeMetadata(m, &p, &metadata)

弱型別輸入

有時候,並不想對結構體欄位型別和map[string]interface{}的對應鍵值做強型別一致的校驗。這時可以使用WeakDecode/WeakDecodeMetadata方法,它們會嘗試做型別轉換:

  • 布林轉字串:true = “1”, false = “0”;
  • 布林轉數位:true = 1, false = 0;
  • 數位轉布林:true if value != 0;
  • 字串轉布林:可接受,
  • 真:1, t, T, TRUE, true, True
  • 假:0, f, F, FALSE, false, False
  • 數位轉字串:自動base10轉換;
  • 負數轉為無符號數(上溢);
  • 字串轉數位:根據字首(如0x等)轉換;
  • 空陣列與空map間互轉;
  • 單個值轉為切片;

逆向轉換

除將map轉換為結構體外,mapstructure也可以將結構體反向解碼為map[string]interface{}。在反向解碼時,我們可以為某些欄位設定mapstructure:“,omitempty”,當這些欄位為預設值時,就不會出現在map中:

  p := &Student{
    Name: "Mike",
    Age:  12,
  }
  var m map[string]interface{}
  mapstructure.Decode(p, &m)

解碼器

mapstructure提供瞭解碼器(Decoder),可靈活方便地控制解碼:

type DecoderConfig struct {
    // 若設定,則在任何解碼或型別轉換(設定了WeaklyTypedInput)前呼叫;對於設定了squash的內嵌欄位,整體呼叫一次;若返回錯誤,則整個解碼失敗
    DecodeHook DecodeHookFunc
    // 若設定,則源資料中存在未使用欄位時,報錯
    ErrorUnused bool
    // 若設定,則有欄位未設定時,報錯
    ErrorUnset bool
    // 若設定,則在設定欄位前先清空(對於map等型別會先清理掉舊資料)
    ZeroFields bool
    // 若設定,支援若型別間的轉換
    WeaklyTypedInput bool
    // Squash will squash embedded structs. 
    Squash bool
    // Metadata is the struct that will contain extra metadata about
    // the decoding. If this is nil, then no metadata will be tracked.
    Metadata *Metadata
    // Result is a pointer to the struct that will contain the decoded
    // value.
    Result interface{}
    // The tag name that mapstructure reads for field names. This
    // defaults to "mapstructure"
    TagName string
    // IgnoreUntaggedFields ignores all struct fields without explicit
    // TagName, comparable to `mapstructure:"-"` as default behaviour.
    IgnoreUntaggedFields bool
    // MatchName is the function used to match the map key to the struct
    // field name or tag. Defaults to `strings.EqualFold`. This can be used
    // to implement case-sensitive tag values, support snake casing, etc.
    MatchName func(mapKey, fieldName string) bool
}

一個支援弱型別轉換的範例:要獲取的結果放到config的result中

    Name string
    Age  int
}
func decoderConfig() {
    m := map[string]interface{}{
        "name": 123,
        "age":  "12",
        "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 {
        log.Printf("Result: %#v", p)
        log.Printf("keys:%#v, unused:%#vn", metadata.Keys, metadata.Unused)
    } else {
        log.Println("decode fail:", err)
    }
}

範例

通過一個messageData結構,action會指示最終的data型別。接收到資料後,先解析出atcion,再根據action轉換為真實的型別。

因time.Time是一個結構體(json序列化時會轉換為時間字串),mapstructure無法正確處理,所以推薦使用時間戳。

為了能正確解析內嵌的DataBasic,需要標記為squash。

import "github.com/mitchellh/mapstructure"
type DataBasic struct {
    DataId     string `json:"dataId"`
    UpdateTime int64  `json:"updateTime"`
}
type AddedData struct {
    DataBasic `mapstructure:",squash"`
    Tag string `json:"tag"`
    AddParams map[string]any `json:"addParams"`
}
type messageData struct {
    Action    int    `json:"action"`
    SeqId     uint64 `json:"seqId"`
    Data      any    `json:"data"`
}
func decodeData() {
    add := &AddedData{
        DataBasic: DataBasic{
            DataId:     "a2",
            UpdateTime: time.Now().UnixMilli(),
        },
        Tag: "tag",
        AddParams:  map[string]any{"dataId": "c2", "otherId": "t2"},
    }
    data := &messageData{
        Action: 1,
        Data:   add,
    }
    js, err := json.Marshal(data)
    if err != nil {
        log.Printf("marshal fail: %v", err)
        return
    }
    got := &messageData{}
    err = json.Unmarshal(js, got)
    if err != nil {
        log.Printf("unmarshal fail: %v", err)
        return
    }
    param := new(AddedData)
    err = mapstructure.Decode(got.Data, param)
    if err != nil {
        log.Printf("unmarshal fail: %v", err)
        return
    }
    log.Printf("param: %+v", param)
}

到此這篇關於Golang中結構體對映mapstructure庫深入詳解的文章就介紹到這了,更多相關Go mapstructure內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com