Как использовать теги Struct в Go

Вступление

Структуры или структуры используются для сбора нескольких фрагментов информации в одном блоке. Этиcollections of information используются для описания концепций более высокого уровня, таких какAddress, состоящий изStreet,City,State иPostalCode. . Когда вы читаете эту информацию из таких систем, как базы данных или API, вы можете использовать теги struct для управления назначением этой информации полям структуры. Теги Struct - это небольшие фрагменты метаданных, прикрепленные к полям структуры, которые предоставляют инструкции для другого кода Go, который работает со структурой.

Как выглядит тег Struct?

Теги структуры 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)
}

Это выведет:

OutputHi! My name is Sammy

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

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

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

Кодировка JSON

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

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

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

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

Попробуйте этот пример, чтобы увидеть, как 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{}

В этой версии мы изменили названия полей для верблюдов. ТеперьName - этоname,Password - этоpassword, и, наконец,CreatedAt - этоcreatedAt. В телеmain мы изменили экземпляр нашей структуры, чтобы использовать эти новые имена. Затем мы передаем структуру функцииjson.MarshalIndent, как и раньше. Результатом на этот раз является пустой объект JSON{}.

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

Использование тегов Struct для управления кодированием

Вы можете изменить предыдущий пример, чтобы экспортируемые поля были правильно закодированы именами полей в верблюжьей оболочке, пометив каждое поле тегом struct. Тег структуры, который распознает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 и"-" не являются стандартами. То, что пакет решит сделать со значениями тега struct, зависит от его реализации. Поскольку пакетencoding/json является частью стандартной библиотеки, другие пакеты также реализовали эти функции таким же образом в рамках соглашения. Тем не менее, важно прочитать документацию для любого стороннего пакета, который использует теги struct, чтобы узнать, что поддерживается, а что нет.

Заключение

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