如何在Go中使用结构标签

介绍

结构结构体用于将多个信息片段集合在一个单元中。这些信息集合用于描述更高级别的概念,例如由StreetCityStatePostalCode组成的Address。当您从数据库或API等系统中读取此信息时,可以使用结构标签来控制如何将此信息分配给结构的字段。结构标签是附加到结构字段的小型元数据片段,为与结构一起工作的其他Go代码提供指令。

结构标签是什么样的?

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

A struct tag looks like this, with the tag offset with backtick ` characters:

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

这个例子定义了一个User类型,其中包含一个Name字段。Name字段被赋予了一个结构标签example:"name"。我们在谈论中将这个特定标签称为“示例结构标签”,因为它使用“example”作为其键。对于Name字段,example结构标签的值为"name"。在User类型上,我们还定义了String()方法,该方法是fmt.Stringer接口所需的。当我们将该类型传递给fmt.Println时,将自动调用此方法,并且给了我们一个机会来生成我们的结构的格式化版本。

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

要使用结构标签来完成某些任务,必须编写其他Go代码来在运行时检查结构。标准库中有使用结构标签作为其操作一部分的包。其中最受欢迎的是encoding/json包。

编码JSON

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

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

此JSON对象包含两个键,languagemascot。在这些键之后是相关联的值。这里 language 键的值是 Go,而 mascot 被赋予值 Gopher

标准库中的JSON编码器利用结构标签作为注释,指示编码器如何命名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" }

我们定义了一个描述用户的结构体,包括他们的姓名、密码和用户创建时间等字段。在main函数中,我们通过为所有字段提供值(除了PreferredFish)来创建该用户的实例(Sammy 喜欢所有的鱼)。然后,我们将User的实例传递给json.MarshalIndent函数。这样做是为了更容易地查看JSON输出,而不需要使用外部格式化工具。这个调用可以用json.Marshal(u)来替换,以打印没有额外空格的JSON。json.MarshalIndent的两个额外参数控制输出的前缀(我们用空字符串省略了)和用于缩进的字符,这里是两个空格字符。任何由json.MarshalIndent产生的错误都会被记录下来,并且程序使用os.Exit(1)终止。最后,我们将json.MarshalIndent返回的[]byte强制转换为string,并将生成的字符串传递给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
{}

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

正确使用驼峰式大小写需要第一个字符为小写。虽然JSON不关心你如何命名字段,但Go关心,因为它指示了字段在包外的可见性。由于encoding/json包是一个与我们使用的main包分开的包,我们必须将第一个字符大写,以使其对encoding/json可见。看起来我们陷入了僵局。我们需要某种方式向JSON编码器传达我们想要将此字段命名为什么。

使用结构标签控制编码

您可以修改前面的示例,通过为每个字段添加结构标记,使导出字段使用驼峰命名方式正确编码。encoding/json识别的结构标记具有键json和控制输出的值。通过将字段名称的驼峰版本作为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结构标记的值部分中,您可以使用,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:结构标签的值参数来实现。

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

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:结构标签。在此示例的输出中,password字段不再存在。

encoding/json包的这些特性——,omitempty"-"以及其他选项——并非标准。一个包决定如何处理结构标签的值取决于其实现。因为encoding/json包是标准库的一部分,其他包也已经按照惯例以相同的方式实现了这些特性。然而,阅读使用结构标签的任何第三方包的文档以了解支持的内容以及不支持的内容是很重要的。

结论

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

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go