跳转到主要内容

介绍


结构或结构用于在一个单元中收集多条信息。这些信息集合用于描述更高级别的概念,例如由街道、城市、州和邮政编码组成的地址。当您从数据库或 API 等系统读取此信息时,您可以使用结构标记来控制如何将此信息分配给结构的字段。结构标签是附加到结构字段的小块元数据,为其他使用该结构的 Go 代码提供指令。

结构标签是什么样的?


Go 结构标记是出现在 Go 结构声明中的类型之后的注释。每个标签都由与某个对应值相关联的短字符串组成。

一个 struct 标签看起来像这样,标签偏移带有反引号 ` 字符:

type User struct {
    Name string `example:"name"`
}


然后其他 Go 代码能够检查这些结构并提取分配给它请求的特定键的值。如果没有检查它们的附加代码,结构标记对代码的操作没有影响。

试试这个例子,看看结构标签是什么样子的,如果没有来自另一个包的代码,它们将不起作用。

package main

import "fmt"

type User struct {
    Name string `example:"name"`
}

func (u *User) String() string {
    return fmt.Sprintf("Hi! My name is %s", u.Name)
}

func main() {
    u := &User{
        Name: "Sammy",
    }

    fmt.Println(u)
}

这将输出:

Output
Hi! My name is Sammy


此示例定义了一个带有名称字段的用户类型。 Name 字段被赋予了一个结构标签 example:"name"。我们将对话中的这个特定标签称为“示例结构标签”,因为它使用“示例”一词作为其键。示例结构标记的名称字段具有值“名称”。在 User 类型上,我们还定义了 fmt.Stringer 接口所需的 String() 方法。这将在我们将类型传递给 fmt.Println 时自动调用,并让我们有机会生成格式良好的结构版本。

在 main 的主体中,我们创建了一个 User 类型的新实例并将其传递给 fmt.Println。即使该结构有一个结构标签,我们看到它对这个 Go 代码的操作没有影响。如果结构标签不存在,它的行为将完全相同。

要使用结构标记来完成某些事情,必须编写其他 Go 代码以在运行时检查结构。标准库的包使用结构标记作为其操作的一部分。其中最流行的是 encoding/json 包。

编码 JSON


JavaScript Object Notation (JSON) 是一种文本格式,用于对在不同字符串键下组织的数据集合进行编码。它通常用于在不同程序之间进行数据通信,因为格式很简单,以至于存在库可以用许多不同的语言对其进行解码。以下是 JSON 的示例:

{
  "language": "Go",
  "mascot": "Gopher"
}


这个 JSON 对象包含两个键,语言和mascot。这些键之后是关联的值。这里语言键的值为 Go,吉祥物的值为 Gopher。

标准库中的 JSON 编码器使用 struct 标签作为注释,向编码器指示您希望如何命名 JSON 输出中的字段。这些 JSON 编码和解码机制可以在 encoding/json 包中找到。

试试这个例子,看看 JSON 是如何在没有结构标签的情况下编码的:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name          string
    Password      string
    PreferredFish []string
    CreatedAt     time.Time
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

这将打印以下输出:

Output
{
  "Name": "Sammy the Shark",
  "Password": "fisharegreat",
  "CreatedAt": "2019-09-23T15:50:01.203059-04:00"
}


我们定义了一个描述用户的结构,其中包含用户名、密码和用户创建时间等字段。在主函数中,我们通过为除 PreferredFish 之外的所有字段(Sammy 喜欢所有鱼)提供值来创建此用户的实例。然后我们将 User 的实例传递给 json.MarshalIndent 函数。使用它是为了让我们可以更轻松地查看 JSON 输出,而无需使用外部格式化工具。可以用 json.Marshal(u) 替换此调用以打印 JSON,而无需任何额外的空格。 json.MarshalIndent 的两个附加参数控制输出的前缀(我们用空字符串省略了),以及用于缩进的字符,这里是两个空格字符。从 json.MarshalIndent 产生的任何错误都会被记录下来,并且程序会使用 os.Exit(1) 终止。最后,我们将从 json.MarshalIndent 返回的 []byte 转换为字符串,并将结果字符串传递给 fmt.Println 以在终端上打印。

结构的字段与命名完全相同。但是,这不是您可能期望的典型 JSON 样式,它使用驼峰式大小写作为字段名称。在下一个示例中,您将更改字段名称以遵循驼峰式大小写样式。正如您在运行此示例时将看到的那样,这将不起作用,因为所需的字段名称与 Go 的有关导出字段名称的规则相冲突。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    name          string
    password      string
    preferredFish []string
    createdAt     time.Time
}

func main() {
    u := &User{
        name:      "Sammy the Shark",
        password:  "fisharegreat",
        createdAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

这将显示以下输出:

Output
{}


在这个版本中,我们将字段名称更改为驼峰式。现在 Name 是 name,Password 是密码,最后 CreatedAt 是 createdAt。在 main 的主体中,我们更改了结构的实例化以使用这些新名称。然后我们像以前一样将结构传递给 json.MarshalIndent 函数。这次的输出是一个空的 JSON 对象 {}。

Camel 大小写字段正确地要求第一个字符小写。虽然 JSON 不关心你如何命名你的字段,但 Go 关心,因为它表示字段在包外的可见性。由于 encoding/json 包是与我们正在使用的主包分开的包,因此我们必须将第一个字符大写以使其对 encoding/json 可见。我们似乎陷入了僵局。我们需要某种方式向 JSON 编码器传达我们希望该字段命名的内容。

使用结构标签控制编码


您可以修改前面的示例,通过使用 struct 标记注释每个字段来使用骆驼大小写的字段名称正确编码的导出字段。 encoding/json 识别的 struct 标签有一个 json 的 key 和一个控制输出的 value。通过将字段名称的驼峰式版本作为 json 键的值,编码器将使用该名称。此示例修复了前两次尝试:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name          string    `json:"name"`
    Password      string    `json:"password"`
    PreferredFish []string  `json:"preferredFish"`
    CreatedAt     time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

这将输出:

Output
{
  "name": "Sammy the Shark",
  "password": "fisharegreat",
  "preferredFish": null,
  "createdAt": "2019-09-23T18:16:17.57739-04:00"
}


我们通过大写名称的第一个字母,将结构字段更改为对其他包可见。但是,这次我们添加了 json:"name" 形式的结构标签,其中 "name" 是我们希望 json.MarshalIndent 在将我们的结构打印为 JSON 时使用的名称。

我们现在已经成功地正确格式化了我们的 JSON。但是请注意,即使我们没有设置这些值,也会打印某些值的字段。如果您愿意,JSON 编码器也可以消除这些字段。

删除空 JSON 字段


禁止输出 JSON 中未设置的字段是很常见的。由于 Go 中的所有类型都有一个“零值”,即它们设置为的一些默认值,因此 encoding/json 包需要额外的信息才能告诉当它假定该零值时某些字段应该被认为是未设置的。在任何 json 结构标记的 value 部分中,您可以在字段的所需名称后加上 ,omitempty ,以告诉 JSON 编码器在字段设置为零值时抑制该字段的输出。以下示例修复了前面的示例,不再输出空字段:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name          string    `json:"name"`
    Password      string    `json:"password"`
    PreferredFish []string  `json:"preferredFish,omitempty"`
    CreatedAt     time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

此示例将输出:

Output
{
  "name": "Sammy the Shark",
  "password": "fisharegreat",
  "createdAt": "2019-09-23T18:21:53.863846-04:00"
}


我们已经修改了前面的示例,以便 PreferredFish 字段现在具有结构标记 json:"preferredFish,omitempty"。 ,omitempty 增强的存在导致 JSON 编码器跳过该字段,因为我们决定不设置它。在我们之前的示例输出中,这具有 null 值。

这个输出看起来好多了,但我们仍在打印用户的密码。 encoding/json 包为我们提供了另一种完全忽略私有字段的方法。

忽略私有字段


某些字段必须从结构中导出,以便其他包可以正确地与该类型交互。但是,这些字段的性质可能很敏感,因此在这些情况下,我们希望 JSON 编码器完全忽略该字段——即使它已设置。这是使用特殊值 - 作为 json: struct 标记的 value 参数来完成的。

此示例修复了暴露用户密码的问题。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name      string    `json:"name"`
    Password  string    `json:"-"`
    CreatedAt time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

运行此示例时,您将看到以下输出:

Output
{
  "name": "Sammy the Shark",
  "createdAt": "2019-09-23T16:08:21.124481-04:00"
}


在这个例子中,我们与之前的例子相比唯一改变的是密码字段现在使用特殊的“-”值作为它的 json: struct 标签。在此示例的输出中,密码字段不再存在。

encoding/json 包的这些特性——、omitempty、“-”和其他选项——不是标准的。包决定如何处理结构标记的值取决于它的实现。因为 encoding/json 包是标准库的一部分,所以其他包也按照惯例以相同的方式实现了这些功能。但是,阅读任何使用结构标记的第三方包的文档以了解支持的内容和不支持的内容非常重要。

结论


结构标签提供了一种强大的方法来增强与您的结构一起使用的代码的功能。许多标准库和第三方包提供了通过使用结构标记来自定义其操作的方法。在您的代码中有效地使用它们提供了这种自定义行为,并简洁地记录了这些字段如何用于未来的开发人员。

 

文章链接