Как использовать теги структуры в Go

Введение

Структуры, или структуры, используются для сбора нескольких кусков информации в одну единицу. Эти коллекции информации используются для описания концепций более высокого уровня, таких как Адрес, состоящий из Улицы, Города, Штата и Почтового индекса. Когда вы читаете эту информацию из систем, таких как базы данных или 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”, потому что он использует слово “example” в качестве своего ключа. Тег структуры example имеет значение "name" для поля Name. Для типа User мы также определяем метод String(), требуемый интерфейсом fmt.Stringer. Он будет вызываться автоматически, когда мы передаем тип в fmt.Println, и дает нам возможность создать красиво отформатированную версию нашей структуры.

Внутри тела main мы создаем новый экземпляр нашего типа User и передаем его в fmt.Println. Даже если у структуры есть тег структуры, мы видим, что это не влияет на работу этого кода на Go. Он будет вести себя точно так же, если бы тег структуры не был указан.

Чтобы использовать теги структур для достижения чего-либо, другой код на Go должен быть написан для изучения структур во время выполнения. В стандартной библиотеке есть пакеты, которые используют теги структур как часть своей работы. Самый популярный из них – это пакет encoding/json.

Кодирование JSON

JSON (JavaScript Object Notation) – это текстовый формат для кодирования коллекций данных, организованных по различным строковым ключам. Обычно он используется для обмена данными между различными программами, так как формат достаточно прост, и существуют библиотеки для его декодирования на многих различных языках программирования. Вот пример JSON:

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

Этот объект JSON содержит два ключа, language и mascot. После этих ключей следуют соответствующие значения. Здесь ключ language имеет значение Go, а mascot присваивается значение Gopher.

Кодировщик JSON в стандартной библиотеке использует теги структур как аннотации, указывающие кодировщику, как вы хотели бы назвать свои поля в выводе JSON. Эти механизмы кодирования и декодирования JSON можно найти в пакете encoding/json package.

Попробуйте этот пример, чтобы увидеть, как 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 (Сэмми любит всех рыб). Затем мы передаем экземпляр User в функцию json.MarshalIndent. Это делается для того, чтобы мы могли более легко увидеть JSON-вывод без использования внешнего инструмента форматирования. Этот вызов можно заменить на json.Marshal(u), чтобы распечатать JSON без дополнительных пробелов. Два дополнительных аргумента для json.MarshalIndent управляют префиксом вывода (который мы опустили с пустой строкой) и символами для выравнивания, которые здесь являются двумя пробелами. Любые ошибки, произведенные из json.MarshalIndent, регистрируются, и программа завершается с использованием os.Exit(1). Наконец, мы преобразуем []byte, возвращенный из json.MarshalIndent, в 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
{}

В этой версии мы изменили имена полей на стиль “camel case”. Теперь Name стал name, Passwordpassword, а CreatedAtcreatedAt. В теле функции main мы изменили создание экземпляра нашей структуры, чтобы использовать эти новые имена. Затем мы передаем структуру в функцию json.MarshalIndent, как и раньше. На этот раз выводом является пустой JSON-объект, {}.

Корректное написание имен полей в стиле “camel case” требует, чтобы первый символ был написан строчными буквами. Хотя JSON не обращает внимания на имена ваших полей, Go делает это, поскольку это указывает на видимость поля за пределами пакета. Поскольку пакет encoding/json является отдельным пакетом от используемого нами пакета main, мы должны написать первый символ заглавной буквой, чтобы сделать его видимым для encoding/json. Кажется, что мы стоим перед тупиком. Нам нужен способ указать JSON-кодировщику, как мы хотели бы назвать это поле.

Использование Тегов Структуры для Управления Кодированием

Вы можете изменить предыдущий пример, чтобы экспортированные поля были правильно закодированы с использованием имен полей в стиле “camel case”, аннотируя каждое поле тегом структуры. Тег структуры, который распознает encoding/json, имеет ключ json и значение, которое контролирует вывод. Разместив версию имени поля в стиле “camel case” в качестве значения ключа 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