<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 每日一庫之 mapstructure

          共 9848字,需瀏覽 20分鐘

           ·

          2020-08-02 08:08

          簡介

          mapstructure用于將通用的map[string]interface{}解碼到對應(yīng)的 Go 結(jié)構(gòu)體中,或者執(zhí)行相反的操作。很多時候,解析來自多種源頭的數(shù)據(jù)流時,我們一般事先并不知道他們對應(yīng)的具體類型。只有讀取到一些字段之后才能做出判斷。這時,我們可以先使用標(biāo)準(zhǔn)的encoding/json庫將數(shù)據(jù)解碼為map[string]interface{}類型,然后根據(jù)標(biāo)識字段利用mapstructure庫轉(zhuǎn)為相應(yīng)的 Go 結(jié)構(gòu)體以便使用。

          快速使用

          本文代碼采用 Go Modules。

          首先創(chuàng)建目錄并初始化:

          $ mkdir mapstructure && cd mapstructure

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

          下載mapstructure庫:

          $ 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)
          ????}
          ??}
          }

          運行結(jié)果:

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

          我們定義了兩個結(jié)構(gòu)體PersonCat,他們的字段有些許不同?,F(xiàn)在,我們約定通信的 JSON 串中有一個type字段。當(dāng)type的值為person時,該 JSON 串表示的是Person類型的數(shù)據(jù)。當(dāng)type的值為cat時,該 JSON 串表示的是Cat類型的數(shù)據(jù)。

          上面代碼中,我們先用json.Unmarshal將字節(jié)流解碼為map[string]interface{}類型。然后讀取里面的type字段。根據(jù)type字段的值,再使用mapstructure.Decode將該 JSON 串分別解碼為PersonCat類型的值,并輸出。

          實際上,Google Protobuf 通常也使用這種方式。在協(xié)議中添加消息 ID 或全限定消息名。接收方收到數(shù)據(jù)后,先讀取協(xié)議 ID 或全限定消息名。然后調(diào)用 Protobuf 的解碼方法將其解碼為對應(yīng)的Message結(jié)構(gòu)。從這個角度來看,mapstructure也可以用于網(wǎng)絡(luò)消息解碼,如果你不考慮性能的話?。

          字段標(biāo)簽

          默認(rèn)情況下,mapstructure使用結(jié)構(gòu)體中字段的名稱做這個映射,例如我們的結(jié)構(gòu)體有一個Name字段,mapstructure解碼時會在map[string]interface{}中查找鍵名name。注意,這里的name是大小寫不敏感的!

          type?Person?struct?{
          ??Name?string
          }

          當(dāng)然,我們也可以指定映射的字段名。為了做到這一點,我們需要為字段設(shè)置mapstructure標(biāo)簽。例如下面使用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)
          ????}
          ??}
          }

          上面代碼中,我們使用標(biāo)簽mapstructure:"username"PersonName字段映射為username,在 JSON 串中我們需要設(shè)置username才能正確解析。另外,注意到,我們將第二個 JSON 串中的Age和第三個 JSON 串中的Name首字母大寫了,但是并沒有影響解碼結(jié)果。mapstructure處理字段映射是大小寫不敏感的。

          內(nèi)嵌結(jié)構(gòu)

          結(jié)構(gòu)體可以任意嵌套,嵌套的結(jié)構(gòu)被認(rèn)為是擁有該結(jié)構(gòu)體名字的另一個字段。例如,下面兩種Friend的定義方式對于mapstructure是一樣的:

          type?Person?struct?{
          ??Name?string
          }

          //?方式一
          type?Friend?struct?{
          ??Person
          }

          //?方式二
          type?Friend?struct?{
          ??Person?Person
          }

          為了正確解碼,Person結(jié)構(gòu)的數(shù)據(jù)要在person鍵下:

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

          我們也可以設(shè)置mapstructure:",squash"將該結(jié)構(gòu)體的字段提到父結(jié)構(gòu)中:

          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 串的不同。

          另外需要注意一點,如果父結(jié)構(gòu)體中有同名的字段,那么mapstructure會將JSON 中對應(yīng)的值同時設(shè)置到這兩個字段中,即這兩個字段有相同的值。

          未映射的值

          如果源數(shù)據(jù)中有未映射的值(即結(jié)構(gòu)體中無對應(yīng)的字段),mapstructure默認(rèn)會忽略它。

          我們可以在結(jié)構(gòu)體中定義一個字段,為其設(shè)置mapstructure:",remain"標(biāo)簽。這樣未映射的值就會添加到這個字段中。注意,這個字段的類型只能為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)
          }

          上面代碼中,我們?yōu)榻Y(jié)構(gòu)體定義了一個Other字段,用于保存未映射的鍵值。輸出結(jié)果:

          other?map[handsome:true?height:1.8m]

          逆向轉(zhuǎn)換

          前面我們都是將map[string]interface{}解碼到 Go 結(jié)構(gòu)體中。mapstructure當(dāng)然也可以將 Go 結(jié)構(gòu)體反向解碼為map[string]interface{}。在反向解碼時,我們可以為某些字段設(shè)置mapstructure:",omitempty"。這樣當(dāng)這些字段為默認(rèn)值時,就不會出現(xiàn)在結(jié)構(gòu)的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))
          }

          上面代碼中,我們?yōu)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">Job字段設(shè)置了mapstructure:",omitempty",且對象pJob字段未設(shè)置。運行結(jié)果:

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

          Metadata

          解碼時會產(chǎn)生一些有用的信息,mapstructure可以使用Metadata收集這些信息。Metadata結(jié)構(gòu)如下:

          //?mapstructure.go
          type?Metadata?struct?{
          ??Keys???[]string
          ??Unused?[]string
          }

          Metadata只有兩個導(dǎo)出字段:

          • Keys:解碼成功的鍵名;
          • Unused:在源數(shù)據(jù)中存在,但是目標(biāo)結(jié)構(gòu)中不存在的鍵名。

          為了收集這些數(shù)據(jù),我們需要使用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結(jié)構(gòu),傳入DecodeMetadata收集解碼的信息。運行結(jié)果:

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

          錯誤處理

          mapstructure執(zhí)行轉(zhuǎn)換的過程中不可避免地會產(chǎn)生錯誤,例如 JSON 中某個鍵的類型與對應(yīng) Go 結(jié)構(gòu)體中的字段類型不一致。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())
          ??}
          }

          上面代碼中,結(jié)構(gòu)體中Person中字段Namestring類型,但輸入中nameint類型;字段Ageint類型,但輸入中agestring類型;字段Emails[]string類型,但輸入中emails[]int類型。故Decode返回錯誤。運行結(jié)果:

          $ 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'

          從錯誤信息中很容易看出哪里出錯了。

          弱類型輸入

          有時候,我們并不想對結(jié)構(gòu)體字段類型和map[string]interface{}的對應(yīng)鍵值做強(qiáng)類型一致的校驗。這時可以使用WeakDecode/WeakDecodeMetadata方法,它們會嘗試做類型轉(zhuǎn)換:

          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對應(yīng)的值123int類型,但是在WeakDecode中會將其轉(zhuǎn)換為string類型以匹配Person.Name字段的類型。同樣的,age的值"18"string類型,在WeakDecode中會將其轉(zhuǎn)換為int類型以匹配Person.Age字段的類型。需要注意一點,如果類型轉(zhuǎn)換失敗了,WeakDecode同樣會返回錯誤。例如將上例中的age設(shè)置為"bad value",它就不能轉(zhuǎn)為int類型,故而返回錯誤。

          解碼器

          除了上面介紹的方法外,mapstructure還提供了更靈活的解碼器(Decoder)??梢酝ㄟ^配置DecoderConfig實現(xiàn)上面介紹的任何功能:

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

          各個字段含義如下:

          • ErrorUnused:為true時,如果輸入中的鍵值沒有與之對應(yīng)的字段就返回錯誤;
          • ZeroFields:為true時,在Decode前清空目標(biāo)map。為false時,則執(zhí)行的是map的合并。用在structmap的轉(zhuǎn)換中;
          • WeaklyTypedInput:實現(xiàn)WeakDecode/WeakDecodeMetadata的功能;
          • Metadata:不為nil時,收集Metadata數(shù)據(jù);
          • Result:為結(jié)果對象,在mapstruct的轉(zhuǎn)換中,Resultstruct類型。在structmap的轉(zhuǎn)換中,Resultmap類型;
          • TagName:默認(rèn)使用mapstructure作為結(jié)構(gòu)體的標(biāo)簽名,可以通過該字段設(shè)置。

          看示例:

          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的方式實現(xiàn)了前面弱類型輸入小節(jié)中的示例代碼。實際上WeakDecode內(nèi)部就是通過這種方式實現(xiàn)的,下面是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內(nèi)部都是先設(shè)置DecoderConfig的對應(yīng)字段,然后創(chuàng)建Decoder對象,最后調(diào)用其Decode方法實現(xiàn)的。

          總結(jié)

          mapstructure實現(xiàn)優(yōu)雅,功能豐富,代碼結(jié)構(gòu)清晰,非常推薦一看!

          大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue?

          參考

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


          推薦閱讀



          學(xué)習(xí)交流 Go 語言,掃碼回復(fù)「進(jìn)群」即可


          站長 polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場和創(chuàng)業(yè)經(jīng)驗


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識

          Go愛好者值得關(guān)注


          瀏覽 46
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  三级视频网站在线观看 | 一级黄色片在线观看 | 亚洲天堂在线视频观看 | 国精产品一区二区三区黑人和中国 | 啪啪视频免费观看 |