曾湘辉 發表於 2023-8-27 22:28:00

github.com/mitchellh/mapstructure 教程

<p>官网链接: github.com/mitchellh/mapstructure</p>
<p>本文只是简单的记录下 mapstructure 库的简单使用,想更加详细的学习,点击 Godoc 学习吧。</p>
<blockquote>
<p>文中内容基本都是来自后面的参考链接。</p>
</blockquote>
<p>github.com/mitchellh/mapstructure是一个用于将通用的map值解码为结构体(struct)并进行错误处理的Go库。当你从某个数据流(如JSON、Gob等)中解码值时,这个库非常有用,因为在读取部分数据之前,你可能不知道底层数据的结构。因此,你可以读取一个<code>mapinterface{} </code>并使用这个库将其解码为适当的本地Go结构体。</p>
<h3 id="1基础使用">1、基础使用</h3>
<p>安装方式:</p>
<pre><code class="language-sh">go get github.com/mitchellh/mapstructure@v1.5.0
</code></pre>
<p>在日常开发中,我们接受的数据可能不是固定的格式,而是会根据某个值的不同有不同的内容。我们来一起看一个例子,更加形象的了解这个库的基础使用。</p>
<pre><code class="language-go">package main

import (
        "encoding/json"
        "fmt"
        "log"

        "github.com/mitchellh/mapstructure"
)

type Person struct {
        Name string
        Ageint
        Jobstring
}

type Cat struct {
        Namestring
        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 mapinterface{}
                err := json.Unmarshal([]byte(data), &amp;m)
                if err != nil {
                        log.Fatal(err)
                }

                switch m["type"].(string) {
                case "person":
                        var p Person
                        mapstructure.Decode(m, &amp;p)
                        fmt.Println("person:", p)

                case "cat":
                        var cat Cat
                        mapstructure.Decode(m, &amp;cat)
                        fmt.Println("cat:", cat)
                }
        }
}

</code></pre>
<p>运行结果:</p>
<pre><code>person: {dj 18 programmer}
cat: {kitty 1 Ragdoll}
</code></pre>
<p>我们定义了两个结构体<code>Person</code>和<code>Cat</code>,他们的字段有些许不同。现在,我们约定通信的 JSON 串中有一个<code>type</code>字段。当<code>type</code>的值为<code>person</code>时,该 JSON 串表示的是<code>Person</code>类型的数据。当<code>type</code>的值为<code>cat</code>时,该 JSON 串表示的是<code>Cat</code>类型的数据。</p>
<p>上面代码中,我们先用<code>json.Unmarshal</code>将字节流解码为<code>mapinterface{}</code>类型。然后读取里面的<code>type</code>字段。根据<code>type</code>字段的值,再使用<code>mapstructure.Decode</code>将该 JSON 串分别解码为<code>Person</code>和<code>Cat</code>类型的值,并输出。</p>
<p>实际上,Google Protobuf 通常也使用这种方式。在协议中添加消息 ID 或<strong>全限定消息名</strong>。接收方收到数据后,先读取协议 ID 或<strong>全限定消息名</strong>。然后调用 Protobuf 的解码方法将其解码为对应的<code>Message</code>结构。从这个角度来看,<code>mapstructure</code>也可以用于网络消息解码,<strong>如果你不考虑性能的话</strong>。</p>
<p>这个例子中,我们可以感受到 mapstructure 库的魅力所在,接下来,我们一起深入的学习如何使用它吧。</p>
<h3 id="2详细学习">2、详细学习</h3>
<h4 id="21field-tags-字段标签">2.1、Field Tags (字段标签)</h4>
<p>在解码为结构体时,<code>mapstructure</code> 默认会使用字段名进行映射。例如,如果一个结构体有一个字段名为 "Username",那么 <code>mapstructure</code> 会在源值中查找键 "username"(<strong>不区分大小写</strong>)。</p>
<pre><code class="language-go">type User struct {
    Username string
}
</code></pre>
<p>通过使用结构体标签来改变 <code>mapstructure</code> 的行为。<code>mapstructure</code> 默认查找的结构体标签是 "mapstructure",但你可以使用 <code>DecoderConfig</code> 进行自定义设置。</p>
<blockquote>
<p>这里一定要注意的是:<code>mapstructure 在字段映射的时候是 case insensitive,即大小写不敏感的。</code></p>
</blockquote>
<h4 id="22renaming-fields">2.2、Renaming Fields</h4>
<p>在实际使用过程中,我们可能需要重命名 <code>mapstructure</code> 查找的键,这个时候,可以使用 "mapstructure" 标签并直接设置一个值。例如,要将上面的 "username" 示例更改为 "user":</p>
<pre><code class="language-go">type User struct {
    Username string `mapstructure:"user"`
}
</code></pre>
<h4 id="23embedded-structs-and-squashing内嵌结构">2.3、Embedded Structs and Squashing(内嵌结构)</h4>
<p>结构体可以任意嵌套,嵌套的结构被认为是拥有该结构体名字的另一个字段。例如,下面两种<code>Friend</code>的定义方式对于<code>mapstructure</code>是一样的:</p>
<pre><code class="language-go">type Person struct {
Name string
}

// 方式一
type Friend struct {
Person
}

// 方式二
type Friend struct {
Person Person
}
</code></pre>
<p>为了正确解码,<code>Person</code>结构的数据要在<code>person</code>键下:</p>
<pre><code class="language-go">mapinterface{} {
"person": mapinterface{}{"name": "dj"},
}
</code></pre>
<p>我们也可以设置<code>mapstructure:",squash"</code>将该结构体的字段提到父结构中:</p>
<pre><code class="language-go">type Friend struct {
Person `mapstructure:",squash"`
}
</code></pre>
<p>这样只需要这样的 JSON 串,无效嵌套<code>person</code>键:</p>
<pre><code class="language-go">mapinterface{}{
"name": "dj",
}
</code></pre>
<p>看<strong>例子1</strong>:</p>
<pre><code class="language-go">package main

import (
        "encoding/json"
        "fmt"
        "log"

        "github.com/mitchellh/mapstructure"
)

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 mapinterface{}
                err := json.Unmarshal([]byte(data), &amp;m)
                if err != nil {
                        log.Fatal(err)
                }

                switch m["type"].(string) {
                case "friend1":
                        var f1 Friend1
                        mapstructure.Decode(m, &amp;f1)
                        fmt.Println("friend1", f1)

                case "friend2":
                        var f2 Friend2
                        mapstructure.Decode(m, &amp;f2)
                        fmt.Println("friend2", f2)
                }
        }
}

</code></pre>
<p>结果:</p>
<pre><code>friend1 {{dj}}
friend2 {{dj2}}
Exiting.
</code></pre>
<p>注意对比<code>Friend1</code>和<code>Friend2</code>使用的 JSON 串的不同。</p>
<p>接着看这个<strong>例子2</strong>:</p>
<pre><code class="language-go">package main

import (
        "encoding/json"
        "fmt"
        "log"

        "github.com/mitchellh/mapstructure"
)

type Person struct {
        Name string
        Type string
}

type Friend1 struct {
        Type string
        Person
}

type Friend2 struct {
        Type   string
        Person `mapstructure:",squash"`
}

func main() {
        datas := []string{`
    {
      "type": "friend1",
      "person": {
      "name":"dj"
      }
    }
`,
                `
    {
      "type": "friend2",
      "name": "dj2"
    }
`,
        }

        for _, data := range datas {
                var m mapinterface{}
                err := json.Unmarshal([]byte(data), &amp;m)
                if err != nil {
                        log.Fatal(err)
                }

                switch m["type"].(string) {
                case "friend1":
                        var f1 Friend1
                        mapstructure.Decode(m, &amp;f1)
                        fmt.Printf("friend1: %+v \n", f1)

                case "friend2":
                        var f2 Friend2
                        mapstructure.Decode(m, &amp;f2)
                        fmt.Printf("friend2: %+v \n", f2)
                }
        }
}

</code></pre>
<p>结果:</p>
<pre><code>friend1: {Type:friend1 Person:{Name:dj Type:}}
friend2: {Type:friend2 Person:{Name:dj2 Type:friend2}}

</code></pre>
<p>例子1和例子2 的区别在于,例子2 中父结构和子结构体中有相同的字段,这个时候,如果为子结构体定义了<code>mapstructure:",squash"</code>的话,那么<code>mapstructure</code>会将JSON 中对应的值<strong>同时设置到这两个字段中</strong>,即这两个字段有相同的值。</p>
<blockquote>
<p>其实这里也跟使用的 JSON字符串的值有关,大家可以自行尝试下,不确定的时候,先写个 demo 看看。</p>
</blockquote>
<h4 id="24remainder-values-未映射的值">2.4、Remainder Values (未映射的值)</h4>
<p>如果在源值中存在任何未映射的键,默认情况下,<code>mapstructure</code> 将会静默地忽略它们(即结构体中无对应的字段)。</p>
<p>你可以通过在 <code>DecoderConfig</code> 中设置 <code>ErrorUnused</code> 来引发错误。如果你正在使用元数据(Metadata),还可以维护一个未使用键的切片(slice)。</p>
<p>你还可以在标签上使用 ",remain" 后缀,将所有未使用的值收集到一个映射(map)中。带有这个标签的字段必须是一个映射类型,只能是 <code> "mapinterface{}" 或 "mapinterface{}"</code> 这两种类型之一。请参阅下面的示例:</p>
<pre><code class="language-go">type Friend struct {
    Namestring
    Other mapinterface{} `mapstructure:",remain"`
}
</code></pre>
<p>加入给定下面的输入,"Other" 字段将会被填充为未使用的其他值(除了 "name" 之外的所有值):</p>
<pre><code class="language-go">mapinterface{}{
    "name":    "bob",
    "address": "123 Maple St.",
}
</code></pre>
<p>完整例子:</p>
<pre><code class="language-go">package main

import (
        "fmt"
        "github.com/mitchellh/mapstructure"
)

type Friend struct {
        Namestring
        Other mapinterface{} `mapstructure:",remain"`
}

func main() {
        m := mapinterface{}{
                "name":    "bob",
                "address": "123 Maple St.",
        }

        var f Friend
        err := mapstructure.Decode(m, &amp;f)
        fmt.Println("err-&gt;", err)
        fmt.Printf("friend: %+v", f)
}


</code></pre>
<p>结果:</p>
<pre><code>err-&gt; &lt;nil&gt;
friend: {Name:bob Other:map}

</code></pre>
<h4 id="25omit-empty-values忽略空值">2.5、Omit Empty Values(忽略空值)</h4>
<p>我们在使用 json 库时,对于空值我们不需要展示的时候,可以使用 <code>"json:,omitempty" </code> 来忽略。 mapstructure 也是一样的。</p>
<p>当从结构体解码到其他任何值时,你可以在标签上使用 ",omitempty" 后缀,以便在该值等于零值时省略它。所有类型的零值在 Go 规范中有明确定义。</p>
<p>例如,数值类型的零值是零("0")。如果结构体字段的值为零且是数值类型,该字段将为空,且不会被编码到目标类型中。</p>
<pre><code class="language-go">type Source struct {
    Age int `mapstructure:",omitempty"`
}
</code></pre>
<h4 id="26unexported-fields">2.6、Unexported fields</h4>
<p>Go 中规定了 未导出的(私有的)结构体字段不能在定义它们的包之外进行设置,解码器将直接跳过它们。</p>
<p>通过以下例子来进行讲解:</p>
<pre><code class="language-go">package main

import (
        "fmt"
        "github.com/mitchellh/mapstructure"
)

type Exported struct {
        private string // this unexported field will be skipped
        Publicstring
}

func main() {
        m := mapinterface{}{
                "private": "I will be ignored",
                "Public":"I made it through!",
        }

        var e Exported
        _ = mapstructure.Decode(m, &amp;e)
        fmt.Printf("e: %+v", e)
}


// 输出
e: {private: Public:I made it through!}
</code></pre>
<h4 id="27other-configuration">2.7、Other Configuration</h4>
<p>mapstructure是高度可配置的。有关支持的其他功能和选项,请参阅 DecoderConfig 结构。</p>
<h4 id="28逆向转换">2.8、逆向转换</h4>
<p>前面我们都是将<code>mapinterface{}</code>解码到 Go 结构体中。<code>mapstructure</code>当然也可以将 Go 结构体反向解码为<code>mapinterface{}</code>。在反向解码时,我们可以为某些字段设置<code>mapstructure:",omitempty"</code>。这样当这些字段为默认值时,就不会出现在结构的<code>mapinterface{}</code>中:</p>
<pre><code class="language-go">type Person struct {
Name string
Ageint
Jobstring `mapstructure:",omitempty"`
}

func main() {
p := &amp;Person{
    Name: "dj",
    Age:18,
}

var m mapinterface{}
mapstructure.Decode(p, &amp;m)

data, _ := json.Marshal(m)
fmt.Println(string(data))
}
</code></pre>
<p>上面代码中,我们为<code>Job</code>字段设置了<code>mapstructure:",omitempty"</code>,且对象<code>p</code>的<code>Job</code>字段未设置。运行结果:</p>
<pre><code>$ go run main.go
{"Age":18,"Name":"dj"}
</code></pre>
<h4 id="29metadata">2.9、Metadata</h4>
<p>解码时会产生一些有用的信息,<code>mapstructure</code>可以使用<code>Metadata</code>收集这些信息。<code>Metadata</code>结构如下:</p>
<pre><code class="language-go">// Metadata 包含关于解码结构的信息,这些信息通常通过其他方式获取起来会比较繁琐或困难。
type Metadata struct {
        // Keys 是成功解码的结构的键
        Keys []string

        // Unused 是一个键的切片,在原始值中被找到,但由于在结果接口中没有匹配的字段,所以未被解码
        Unused []string

        // Unset 是一个字段名称的切片,在结果接口中被找到,
        // 但在解码过程中未被设置,因为在输入中没有匹配的值
        Unset []string
}

</code></pre>
<p><code>Metadata</code>只有3个导出字段:</p>
<ul>
<li><code>Keys</code>:解码成功的键名;</li>
<li><code>Unused</code>:在源数据中存在,但是目标结构中不存在的键名。</li>
<li><code>Unset</code>:在目标结构中存在,但是源数据中不存在。</li>
</ul>
<p>为了收集这些数据,我们需要使用<code>DecodeMetadata</code>来代替<code>Decode</code>方法:</p>
<p>接下来我们一起看个例子来进行学习:</p>
<pre><code class="language-go">package main

import (
        "fmt"
        "github.com/mitchellh/mapstructure"
)

type Person struct {
        Name string
        Ageint
        Sexbool
}

func main() {
        m := mapinterface{}{
                "name": "dj",
                "age":18,
                "job":"programmer",
        }

        var p Person
        var metadata mapstructure.Metadata
        mapstructure.DecodeMetadata(m, &amp;p, &amp;metadata)

        fmt.Printf("keys:%#v unused:%#v, unset: %#v \n", metadata.Keys, metadata.Unused, metadata.Unset)
}


// 结果
keys:[]string{"Name", "Age"} unused:[]string{"job"}, unset: []string{"Sex"}
</code></pre>
<h4 id="210错误处理">2.10、错误处理</h4>
<p><code>mapstructure</code>执行转换的过程中不可避免地会产生错误,例如 JSON 中某个键的类型与对应 Go 结构体中的字段类型不一致。<code>Decode/DecodeMetadata</code>会返回这些错误:</p>
<pre><code class="language-go">type Person struct {
Name   string
Age    int
Emails []string
}

func main() {
m := mapinterface{}{
    "name":   123,
    "age":    "bad value",
    "emails": []int{1, 2, 3},
}

var p Person
err := mapstructure.Decode(m, &amp;p)
if err != nil {
    fmt.Println(err.Error())
}
}
</code></pre>
<p>上面代码中,结构体中<code>Person</code>中字段<code>Name</code>为<code>string</code>类型,但输入中<code>name</code>为<code>int</code>类型;字段<code>Age</code>为<code>int</code>类型,但输入中<code>age</code>为<code>string</code>类型;字段<code>Emails</code>为<code>[]string</code>类型,但输入中<code>emails</code>为<code>[]int</code>类型。故<code>Decode</code>返回错误。运行结果:</p>
<pre><code class="language-sh">$ go run main.go
5 error(s) decoding:

* 'Age' expected type 'int', got unconvertible type 'string'
* 'Emails' expected type 'string', got unconvertible type 'int'
* 'Emails' expected type 'string', got unconvertible type 'int'
* 'Emails' expected type 'string', got unconvertible type 'int'
* 'Name' expected type 'string', got unconvertible type 'int'
</code></pre>
<p>从错误信息中很容易看出哪里出错了。</p>
<h4 id="211弱类型输入">2.11、弱类型输入</h4>
<p>有时候,我们并不想对结构体字段类型和<code>mapinterface{}</code>的对应键值做强类型一致的校验。这时可以使用<code>WeakDecode/WeakDecodeMetadata</code>方法,它们会尝试做类型转换:</p>
<pre><code class="language-go">type Person struct {
Name   string
Age    int
Emails []string
}

func main() {
m := mapinterface{}{
    "name":   123,
    "age":    "18",
    "emails": []int{1, 2, 3},
}

var p Person
err := mapstructure.WeakDecode(m, &amp;p)
if err == nil {
    fmt.Println("person:", p)
} else {
    fmt.Println(err.Error())
}
}
</code></pre>
<p>虽然键<code>name</code>对应的值<code>123</code>是<code>int</code>类型,但是在<code>WeakDecode</code>中会将其转换为<code>string</code>类型以匹配<code>Person.Name</code>字段的类型。同样的,<code>age</code>的值<code>"18"</code>是<code>string</code>类型,在<code>WeakDecode</code>中会将其转换为<code>int</code>类型以匹配<code>Person.Age</code>字段的类型。 需要注意一点,如果类型转换失败了,<code>WeakDecode</code>同样会返回错误。例如将上例中的<code>age</code>设置为<code>"bad value"</code>,它就不能转为<code>int</code>类型,故而返回错误。</p>
<h4 id="212解码器">2.12、解码器</h4>
<p>除了上面介绍的方法外,<code>mapstructure</code>还提供了更灵活的解码器(<code>Decoder</code>)。可以通过配置<code>DecoderConfig</code>实现上面介绍的任何功能:</p>
<pre><code class="language-go">// DecoderConfig 是用于创建新解码器的配置,允许自定义解码的各个方面。
type DecoderConfig struct {
        // DecodeHook,如果设置了,将在任何解码和任何类型转换(如果 WeaklyTypedInput 打开)之前调用。
        // 这允许你在将值设置到结果结构之前修改它们的值。
        // DecodeHook 会为输入中的每个映射和值调用一次。这意味着如果结构体具有带有 squash 标签的嵌入字段,
        // 解码钩子只会一次使用所有输入数据进行调用,而不是为每个嵌入的结构体分别调用。
        //
        // 如果返回错误,整个解码将以该错误失败。
        DecodeHook DecodeHookFunc

        // 如果 ErrorUnused 为 true,则表示在解码过程中存在于原始映射中但未被使用的键是错误的(多余的键)。
        ErrorUnused bool

        // 如果 ErrorUnset 为 true,则表示在解码过程中存在于结果中但未被设置的字段是错误的(多余的字段)。
        // 这仅适用于解码为结构体。这还将影响所有嵌套结构体。
        ErrorUnset bool

        // ZeroFields,如果设置为 true,在写入字段之前将字段清零。
        // 例如,一个映射在放入解码值之前将被清空。如果为 false,映射将会被合并。
        ZeroFields bool

        // 如果 WeaklyTypedInput 为 true,则解码器将进行以下“弱”转换:
        //
        //   - 布尔值转换为字符串(true = "1",false = "0")
        //   - 数字转换为字符串(十进制)
        //   - 布尔值转换为 int/uint(true = 1,false = 0)
        //   - 字符串转换为 int/uint(基数由前缀隐含)
        //   - int 转换为布尔值(如果值 != 0 则为 true)
        //   - 字符串转换为布尔值(接受:1、t、T、TRUE、true、True、0、f、F、
        //   FALSE、false、False。其他任何值都是错误的)
        //   - 空数组 = 空映射,反之亦然
        //   - 负数转换为溢出的 uint 值(十进制)
        //   - 映射的切片转换为合并的映射
        //   - 单个值根据需要转换为切片。每个元素都会被弱解码。
        //   例如:"4" 如果目标类型是 int 切片,则可以变为 []int{4}。
        //
        WeaklyTypedInput bool

        // Squash 将压缩(squash)嵌入的结构体。也可以通过使用标签将 squash 标签添加到单个结构体字段中。例如:
        //
        //type Parent struct {
        //      Child `mapstructure:",squash"`
        //}
        Squash bool

        // Metadata 是将包含有关解码的额外元数据的结构。
        // 如果为 nil,则不会跟踪任何元数据。
        Metadata *Metadata

        // Result 是指向将包含解码值的结构体的指针。
        Result interface{}

        // 用于字段名称的标签名称,mapstructure 会读取它。默认为 "mapstructure"。
        TagName string

        // IgnoreUntaggedFields 忽略所有没有明确 TagName 的结构字段,类似于默认行为下的 `mapstructure:"-"`。
        IgnoreUntaggedFields bool

        // MatchName 是用于匹配映射键与结构体字段名或标签的函数。
        // 默认为 `strings.EqualFold`。可以用来实现区分大小写的标签值、支持蛇形命名等。
        MatchName func(mapKey, fieldName string) bool
}

</code></pre>
<p>例子:</p>
<pre><code>type Person struct {
Name string
Ageint
}

func main() {
m := mapinterface{}{
    "name": 123,
    "age":"18",
    "job":"programmer",
}

var p Person
var metadata mapstructure.Metadata

decoder, err := mapstructure.NewDecoder(&amp;mapstructure.DecoderConfig{
    WeaklyTypedInput: true,
    Result:         &amp;p,
    Metadata:         &amp;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())
}
}
</code></pre>
<p>这里用<code>Decoder</code>的方式实现了前面弱类型输入小节中的示例代码。实际上<code>WeakDecode</code>内部就是通过这种方式实现的,下面是<code>WeakDecode</code>的源码:</p>
<pre><code class="language-go">// mapstructure.go
func WeakDecode(input, output interface{}) error {
config := &amp;DecoderConfig{
    Metadata:         nil,
    Result:         output,
    WeaklyTypedInput: true,
}

decoder, err := NewDecoder(config)
if err != nil {
    return err
}

return decoder.Decode(input)
}

</code></pre>
<p>再实际上,<code>Decode/DecodeMetadata/WeakDecodeMetadata</code>内部都是先设置<code>DecoderConfig</code>的对应字段,然后创建<code>Decoder</code>对象,最后调用其<code>Decode</code>方法实现的。</p>
<h4 id="参考链接">参考链接:</h4>
<p>Godoc</p>
<p>Go 每日一库之 mapstructure</p><br><br>
来源:https://www.cnblogs.com/huageyiyangdewo/p/17661013.html
頁: [1]
查看完整版本: github.com/mitchellh/mapstructure 教程