Структурные теги в Go

Структуры в Go используются для сбора нескольких фрагментов информации в один блок. Эти коллекции информации используются для описания понятий более высокого уровня: так же, как адрес состоит из улицы, города, области и почтового индекса. При чтении этой информации из таких систем, как базы данных или API, вы можете использовать теги. Теги управляют назначением этой информации и присваивают ее полям структуры. Структурные теги – это небольшие фрагменты метаданных, закрепленные за полями структуры; теги содержат инструкции для другого кода Go, который работает с данной структурой.

Читайте также: Определение структур в Go

Как выглядит структурный тег?

Структурные теги Go представляют собой аннотации, которые появляются в объявлении структуры Go после типа. Каждый тег состоит из коротких строк, связанных с некоторым соответствующим значением.

Структурный тег выглядит следующим образом:

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)
}

Этот код вернет:

Hi! My name is Sammy

В этом примере определяется тип User с полем Name. Поле Name было присвоено структурному тегу example:»name». Структурный тег example имеет значение «name» для поля Name. Для типа User мы определяем метод String(), требуемый интерфейсом fmt.Stringer. Он вызовется автоматически, когда мы передадим тип в fmt.Println, что даст возможность создать красиво отформатированную версию нашей структуры.

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

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

Пакет encoding/json

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

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

Этот объект JSON содержит два ключа, language и mascot. После этих ключей идут соответствующие значения. Здесь ключ 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))
}

Вы получите такой вывод:

{
"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, логируются, и программа Затем мы, мы преобразуем []byte (возвращенный из json.MarshalIndent) в строку и передаем полученную строку в fmt.Println для вывода в терминал.

Поля структуры отображаются точно так, как мы их назвали. Это не типичный стиль JSON, который использует верблюжий регистр для полей. В следующем примере мы попробуем изменить имена полей в соответствии со стандартным стилем 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))
}

Вы получите такой вывод:

{}

В этом примере мы отредактировали имена полей, чтобы они были записаны в верблюжьем регистре: теперь поле Name записано как name, Password – как 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))
}

Вывод получится такой:

{
"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

Чаще всего вывод полей с пустыми значениями подавляется. Поскольку все типы в Go имеют нулевое значение, то есть какое-то значение по умолчанию, которое им задано, пакету encoding/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))
}

Вывод будет таким:

{
"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.

Вот так выглядит код, который будет скрывать пароль пользователя.

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))
}

Запустив его, вы получите такой результат:

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

Единственное, что мы изменили в этом примере по сравнению с предыдущими – мы внесли специальное значение «-» в поле пароля для тега json: struct. Мы видим, что в выходных данных поле пароля больше не отображается.

Функции пакета encoding/json, omitempty и значение «-» не являются стандартами. Что пакет решит сделать со значениями структурного тега, зависит от его реализации. Поскольку пакет encoding/json является частью стандартной библиотеки, другие пакеты также реализовали эти функции по соглашению. Важно читать документацию всех сторонних пакетов, которые используют структурные теги, чтобы узнать, что поддерживается пакетами, а что нет.

Заключение

Структурные теги предлагают мощные средства для расширения функциональности кода, который работает с вашими структурами. Многие стандартные библиотеки и сторонние пакеты позволяют настроить их работу с помощью этих тегов. Эффективное использование тегов в вашем коде обеспечивает необходимое поведение и лаконично документирует, как эти поля используются в программе.

Tags: , ,