Структура в Go представляет собой тип данных с заданным набором атрибутов (полей), использующийся для описания составных объектов. Структура имеет близкие аналоги в других языках программирования:

  • C — struct;
  • С++ — class, struct;
  • Python — class, tuple;
  • PHP — class;
  • Lua — table.

Посмотрите, как выглядит описание типа Person:

type Person struct {
    Name        string
    Email       string
    dateOfBirth time.Time
} 

Здесь описан тип структуры, отображающий информацию о некотором человеке: его имя, электронную почту, дату рождения. Поля структуры могут быть любого типа, доступного в языке.

Могут быть и указателями на саму структуру. Классический пример — структура данных «дерево»:

type Tree struct {
    Value      int
    LeftChild  *Tree
    RightChild *Tree
} 

Инициализация

Существует несколько подходов к созданию экземпляра объекта.

1. Пустой объект

p := Person{}
// или
var p Person 

Все поля структуры при таком подходе принимают значения по умолчанию.

Подход применяют:

  • когда экземпляр не требует специальной инициализации и может быть использован дальше по коду;
  • когда для инициализации полей нужны дополнительные условия и данные, то есть выставление значений конкретных полей будет следовать ниже по коду.

2. Неявное указание значений полей

date := time.Date(2000, 12, 1, 0, 0, 0, 0, time.UTC)
p := Person{"Иван", "ivan@yandex.ru", date} 

При таком подходе перечисляют значения для всех полей структуры, используя литералы либо значения внешних переменных.

Требования:

  • Нужно перечислить все поля объекта.
  • Порядок следования аргументов инициализатора должен совпадать с порядком описания полей структуры. Если поставить поле Email на первое место в описании type Person struct, инициализация экземпляра выше будет некорректна (с точки зрения логики, но не компилятора).

Подход применяют:

  • когда нужно явно указать значения всех полей объекта;
  • когда вы уверены, что спецификация типа не будет меняться часто, иначе придётся вносить правки для каждого инициализатора объекта в коде.

3. Явное указание значений полей

p := Person{Name: "Иван", Email: "ivan@yandex.ru"} 

При таком подходе явно указывают имена полей и их значения.

Особенности:

  • этот подход отличается от первого опциональным указанием полей;
  • порядок указания полей не важен;
  • значения полей, которые не были использованы в инициализаторе (dateOfBirth в примере), примут значения по умолчанию.

Для повышения читабельности кода такую инициализацию часто описывают в несколько строк, что справедливо и для второго подхода:

p := Person{
    Name:  "Иван",
    Email: "ivan@yandex.ru",
} 

Note

Обратите внимание, что последняя строка в многострочной записи литерала тоже заканчивается запятой. Это делается для того, чтобы можно было вставлять и удалять строки, не заботясь о запятой в конце.

Подход применяют:

  • почти всегда, так как он лишён ограничений, описанных выше.

На практике обычно применяется явное указание имён, потому что оно снижает количество возможных ошибок.

4. Конструктор

Учитывая тонкости при инициализации сложного объекта, разработчики применяют конструкторы.

В Go нет синтаксиса конструкторов и деструкторов, но часто можно встретить аналог:

   func NewPerson(name, email string, dobYear, dobMonth, dobDay int) Person {
       return Person{
           Name:        name,
           Email:       email,
           dateOfBirth: time.Date(dobYear, time.Month(dobMonth), dobDay, 0, 0, 0, 0, time.UTC),
       }
   } 

Вот некоторые правила, одобренные Go-сообществом:

  • имя функции конструктора пишут с префиксом New;
  • если конструктор производит валидацию аргументов, функция должна возвращать ошибку последним аргументом.

Можем вернуться к нашему примеру, чтобы добавить проверку корректности email и числовых компонент даты, тогда декларация функции примет вид:

func NewPerson(name, email string, dobYear, dobMonth, dobDay int) (Person, error) {} 

Подход применяют:

  • когда нужно производить валидацию аргументов, чтобы построить логически правильный объект;
  • когда построение экземпляра объекта требует дополнительных действий, например, подключения к базе данных.

Note

Пример выше не идеален, так как изменение спецификации Person потребует изменения прототипа конструктора или создания новой версии (скажем, NewPersonWithPhone()). Идиоматичные Go-подходы к созданию объектов рассмотрим в следующих темах.

Доступ к полям

Для доступа к полям структуры используется точка (p.Name):

p := NewPerson("Иван", "ivan@yandex.ru", 2000, 12, 1)
fmt.Println(p.Name, p.Email)
 
p.Name = "Пётр"
fmt.Println(p.Name) 
Иван ivan@yandex.ru
Пётр 

Область видимости

Как вы уже знаете, в Go есть понятия экспортируемых и неэкспортируемых типов. Код разделяется на пакеты, и, чтобы тип, функция или глобальная переменная были доступны в другом пакете, их имена должны начинаться с заглавной буквы. Это же правило работает для полей и методов структуры.

В примере выше Person — экспортируемый тип (публичный). Другие пакеты могут создавать экземпляры этого типа и иметь доступ к публичным полям Name и Email. А поле dateOfBirth — неэкспортируемое (приватное).

Note

Неэкспортируемый тип можно использовать в другом пакете, если есть соответствующий конструктор типа и экспортируемая функция конструктора. Такой трюк встречается в Go-коде, однако чаще всего для сокрытия реализации используют интерфейсы.

Приведём пример экспортирования приватного типа:

package foo
 
// privateFoo — неэкспортируемый тип
type privateFoo struct {
    Value string
}
 
// NewPrivateFoo — конструктор типа privateFoo
// Функция публичная, то есть может быть вызвана из других пакетов
func NewPrivateFoo() privateFoo {
    return privateFoo{Value: "some data"}
} 
package main
 
import "github.com/the_greatest_coder/hello_go/foo"
 
func main() {
    // f := foo.privateFoo{} // ошибка компиляции
    f := foo.NewPrivateFoo()
    fmt.Println(f.Value) // поле Value экспортируемое, то есть его можно использовать
} 
some data 

Теги

У каждого поля структуры может быть набор аннотаций, которые называются тегами (tags):

type GetUserRequest struct {
    UserId string `json:"user_id" yaml: "user_id" format:"uuid" example:"2e263a90-b74b-11eb-8529-0242ac130003"`
    IsDeleted *bool `json:"is_deleted,omitempty" yaml:"is_deleted"`
} 

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

Набор тегов с их значениями можно представить как набор ключей и значений, где ключи разделяются пробелами, а значения ключей — запятой.

В примере выше встречаются следующие теги:

  • json — используется пакетом encoding/json для сериализации/десерилизации структур в JSON;
  • yaml — похож на json, но используется внешними библиотеками для работы с форматом YAML;
  • format и example — могут быть как подсказкой для разработчика, так и аннотацией для генерации Swagger-описания (к примеру, библиотекой swag).

Применяемые аннотации чаще всего зависят от используемой библиотеки. Возможные ключи и значения стоит искать в документации пакета (в худшем случае — в коде).

Разработчик может вводить свои теги и работать с ними через пакет reflect стандартной библиотеки.

Пример

Для сериализации используется функция json.Marshal() пакета json. Дана структура:

type Person struct {
    Name        string
    Email       string
    DateOfBirth time.Time
} 

Напишите код, который будет сериализовывать структуру в json-строку следующего вида:

{
  "Имя": "Aлекс",
  "Почта": "alex@yandex.ru"
} 

Решение:

type Person struct {
    Name        string    `json:"Имя"`
    Email       string    `json:"Почта"`
    DateOfBirth time.Time `json:"-"` // - означает, что это поле не будет сериализовано
}
 
func main() {
    man := Person{
        Name:        "Alex",
        Email:       "alex@yandex.ru",
        DateOfBirth: time.Now(),
    }
    jsMan, err := json.Marshal(man)
    if err != nil {
        log.Fatalln("unable marshal to json")
    }
    fmt.Printf("Man %v", string(jsMan)) // Man {"Имя":"Alex","Почта":"alex@yandex.ru"}
}

Анонимные структуры

Анонимные структуры объявляются и используются непосредственно в коде. Отдельный тип для них не описывают, потому что анонимные структуры применяются однократно, и описание имеет смысл только для конкретной части кода: например, при сериализации/десериализации сообщений. Чаще всего анонимные структуры используют в тестах для описания тестовых структур.

Проще всего понять концепцию анонимных структур из следующего соображения:

type Person struct {
    Name string 
} 

Конструкция type Person ... на самом деле не описывает, а создаёт тип на основе существующего и называет его. То есть по факту тип создаёт именно конструкция struct{}.

Получив такой анонимный тип, можно сразу же создать переменную этого типа.

Приведём пример использования анонимной структуры при построении REST-запроса:

req := struct {
    NameContains string `json:"name_contains"`
    Offset       int    `json:"offset"`
    Limit        int    `json:"limit"`
}{
    NameContains: "Иван",
    Limit:        50,
}
 
reqRaw, _ := json.Marshal(req)
fmt.Println(string(reqRaw)) 
{"name_contains":"Иван","offset":0,"limit":50} 

Здесь мы описали анонимную структуру, инициализировали её экземпляр, произвели JSON-сериализацию и вывели результат в виде строки.

struct{}

var c struct{}
// или
c := struct{}{}
 
fmt.Println(unsafe.Sizeof(c))
fmt.Println(unsafe.Pointer(&c)) 
0
0x11d46e8 

Размер struct{} равен 0, при этом объект c имеет адрес. Такую лазейку можно использовать для оптимизации кода по памяти, а в дальнейшем разберём это на практике.

Встраивание структур (struct embedding)

Go supports embedding of structs and interfaces to express a more seamless composition of types. This is not to be confused with //go:embed which is a go directive introduced in Go version 1.16+ to embed files and folders into the application binary.

package main
 
import "fmt"
 
type base struct {
    num int
}
 
func (b base) describe() string {
    return fmt.Sprintf("base with num=%v", b.num)
}
 
// A `container` _embeds_ a `base`.
// An embedding looks like a field without a name.
type container struct {
    base
    str string
}
 
func main() {
 
    co := container{
    // When creating structs with literals, we have to initialize the embedding explicitly; here the embedded type serves as the field name.
        base: base{
            num: 1,
        },
        str: "some name",
    }
 
    fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)
 
    fmt.Println("also num:", co.base.num)
 
    fmt.Println("describe:", co.describe())
 
    type describer interface {
        describe() string
    }
 
	// Embedding structs with methods may be used to bestow interface implementations onto other structs. Here we see that a `container` now implements the `describer` interface because it embeds `base`.
    var d describer = co
    fmt.Println("describer:", d.describe())
}

Reference: https://gobyexample.com/struct-embedding

Сравнение структур и их аналогов в популярных языках программирования

В таблице приведены особенности структур языка Go в сравнении с другими языками.

Пример: Реверс-инжиниринг JSON

Есть пример API-вызова в формате JSON:

{
    "header": {
        "code": 0,
        "message": ""
    },
    "data": [{
        "type": "user",
        "id": 100,
        "attributes": {
            "email": "bob@yandex.ru",
            "article_ids": [10, 11, 12]
        }
    }]
} 

На входе есть строка с сырыми данными, требуется написать функцию её десериализации:

type Response struct {
    // поля с тегами
}
 
func ReadResponse(rawResp string) (Response, error) {
    // код десериализации
} 

Решение:

package main
 
import (
    "encoding/json"
    "fmt"
)
 
const rawResp = `
{
    "header": {
        "code": 0,
        "message": ""
    },
    "data": [{
        "type": "user",
        "id": 100,
        "attributes": {
            "email": "bob@yandex.ru",
            "article_ids": [10, 11, 12]
        }
    }]
}
`
 
type (
    Response struct {
        Header ResponseHeader `json:"header"`
        Data   ResponseData   `json:"data,omitempty"`
    }
 
    ResponseHeader struct {
        Code    int    `json:"code"`
        Message string `json:"message,omitempty"`
    }
 
    ResponseData []ResponseDataItem
 
    ResponseDataItem struct {
        Type       string                `json:"type"`
        Id         int                   `json:"id"`
        Attributes ResponseDataItemAttrs `json:"attributes"`
    }
 
    ResponseDataItemAttrs struct {
        Email      string `json:"email"`
        ArticleIds []int  `json:"article_ids"`
    }
)
 
func ReadResponse(rawResp string) (Response, error) {
    resp := Response{}
    if err := json.Unmarshal([]byte(rawResp), &resp); err != nil {
        return Response{}, fmt.Errorf("JSON unmarshal: %w", err)
    }
 
    return resp, nil
}
 
func main() {
    resp, err := ReadResponse(rawResp)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", resp)
}

📂 Go | Последнее изменение: 11.11.2024 20:26