如何在Go中使用Struct標籤

介紹

結構,或稱為structs,用於將多個信息片段集合在一個單元中。這些信息集合用於描述更高層次的概念,例如由StreetCityStatePostalCode組成的Address。當您從資料庫或API等系統中讀取這些信息時,可以使用struct tags來控制如何將此信息分配給結構的字段。Struct tags是附加到struct字段的小型元數據,它提供了對與該結構一起工作的其他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”作為其鍵。 example結構標籤對於Name字段的值為"name"。在User類型上,我們還定義了String()方法,該方法是fmt.Stringer接口所需的。當我們將類型傳遞給fmt.Println時,將自動調用此方法,並且我們有機會生成我們的結構的格式良好的版本。

main的主體內,我們創建了User類型的新實例並將其傳遞給fmt.Println。儘管結構中有一個結構標籤,但我們看到它對這段Go代碼的操作沒有影響。如果結構標籤不存在,它將完全相同地運行。

要使用結構標籤來完成某事,必須編寫其他Go代碼來在運行時檢查結構。標準庫中有使用結構標籤作為其操作的一部分的套件。其中最流行的是encoding/json套件。

編碼JSON

JavaScript Object Notation(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
{}

在這個版本中,我們已經將字段的名稱改為駝峰命名法。現在Name變成了namePassword變成了password,最後CreatedAt變成了createdAt。在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