Структуры в Go

Теория: Поля структуры и их типы

В Go структура — это способ собрать связанные данные в единый объект. Каждый элемент внутри структуры называется полем. Поле имеет имя и тип, и именно это делает работу с данными безопасной и предсказуемой.

Проблема без структур

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

postIDs := []int{1, 2}
titles := []string{"Go vs Python", "Как работает GC"}
authors := []string{"Иван", "Мария"}
likes := []int{120, 45}

// Чтобы вывести пост, приходится помнить индексы
fmt.Println(postIDs[0], titles[0], authors[0], likes[0])

Здесь мы пытаемся собрать информацию о постах, но она разнесена по разным срезам. Нужно постоянно помнить индексы: если где-то ошибиться, данные «поедут». Например, пост с названием «Go vs Python» может оказаться у Марии и с неправильным количеством лайков. Это типичный источник ошибок.

Решение с помощью структур

Опишем сущность поста как структуру:

type Post struct {
	ID     int
	Title  string
	Author string
	Likes  int
}

Теперь мы явно говорим: у каждого поста есть ID, заголовок, автор и лайки. Создадим список постов:

posts := []Post{
	{ID: 1, Title: "Go vs Python", Author: "Иван", Likes: 120},
	{ID: 2, Title: "Как работает GC", Author: "Мария", Likes: 45},
}

fmt.Println(posts[0].Author) // Иван

Мы собрали все данные о посте в одном месте. Теперь, чтобы получить автора первого поста, достаточно обратиться к posts[0].Author. Код стал проще и надёжнее.

Примеры полей и их типов

Поля могут быть разных типов. Например, товар в интернет-магазине:

type Product struct {
	Name  string   // строка: название товара
	Price float64  // число с плавающей точкой: цена
	Stock int      // целое число: остаток на складе
	Tags  []string // срез строк: набор тегов для поиска
}

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

Более сложная структура — корзина:

type Cart struct {
	Owner   string
	Items   []Product      // срез структур. Срез — это динамический массив: его размер можно изменять с помощью append, и если мы передадим часть среза дальше по коду, он всё равно будет ссылаться на тот же массив.
	Coupons map[string]int // карта промокодов и скидок
}

В Cart мы связали сразу несколько сущностей: владелец корзины, список товаров и карта с промокодами. Это удобно: теперь корзина — это полноценный объект с данными.

Практический кейс

До структур: в сервисе доставки еда, ресторан, список блюд и адреса клиентов хранились в отдельных срезах.

restaurants := []string{"Пиццерия", "Суши-бар"}
orders := [][]string{
	{"Маргарита", "Кола"},
	{"Филадельфия", "Чай"},
}

addresses := []string{"ул. Ленина 10", "ул. Гагарина 5"}

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

После структур: всё собрано в объект Delivery.

type Delivery struct {
	Restaurant string // название ресторана
	Items []string // блюда
	Address string // адрес доставки
}

deliveries := []Delivery{
	{Restaurant: "Пиццерия", Items: []string{"Маргарита", "Кола"}, Address: "ул. Ленина 10"},
	{Restaurant: "Суши-бар", Items: []string{"Филадельфия", "Чай"}, Address: "ул. Гагарина 5"},
}

fmt.Println(deliveries[0].Address) // ул. Ленина 10

Теперь каждая доставка — это единый объект. Чтобы узнать адрес первой доставки, мы пишем deliveries[0].Address. Ошибиться невозможно: данные связаны внутри структуры.

Экспортируемые и приватные поля

В Go видимость поля определяется первой буквой его имени:

  • Заглавная (ID, Name) — экспортируемое поле, доступно из других пакетов.
  • Строчная (balance, passwordHash) — приватное поле, доступно только внутри текущего пакета.

Это не «инкапсуляция по рантайму», а правило компилятора: другие пакеты просто не могут обратиться к приватному имени.

Базовый пример

package billing

type Account struct {
	ID      int     // экспортируется
	balance float64 // приватно
}

func (a *Account) Deposit(amount float64) { // экспортируемый метод
	if amount <= 0 {
		return
	}
	a.balance += amount
}

func (a *Account) Balance() float64 { // только чтение извне
	return a.balance
}

Что сделали и зачем: скрыли balance, чтобы запретить прямое присваивание снаружи и обеспечить инварианты (нельзя сделать отрицательное пополнение). Внешний код видит только безопасные операции.

Экспорт/приватность и JSON/YAML, ORM, рефлексия

Большинство пакетов, работающих через рефлексию (encoding/json, yaml, mapstructure, ORM/библиотеки), видят только экспортируемые поля. Приватные будут проигнорированы, даже если написали теги.

type UserDTO struct {
	ID    int    `json:"id"` // будет сериализован
	Name  string `json:"name"`
	email string `json:"email"` // не сериализуется: поле приватное
}

Вывод: если нужно (де)сериализовать поле — делайте его экспортируемым и управляйте именем через теги (json:"field_name,omitempty").

Паттерн: «умный конструктор» + приватные поля

Скрываем детали, отдаём только валидные объекты.

package auth

type User struct {
	ID           int
	Email        string
	passwordHash []byte // скрыто от внешнего мира
}

// NewUser валидирует вход и сразу устанавливает корректный hash.
func NewUser(id int, email, password string) (*User, error) {
	if !isValidEmail(email) {
		return nil, ErrEmail
	}

	hash := hashPassword(password)
	return &User{ID: id, Email: email, passwordHash: hash}, nil
}

func (u *User) CheckPassword(password string) bool {
	return compareHash(u.passwordHash, password)
}

Зачем: запрещаем создавать «полупустых» пользователей и хранить сырой пароль. Внешний код не сможет случайно прочитать или записать passwordHash.

Паттерн: публичный DTO + приватная доменная модель

Разделяем типы для API и для доменной логики.

package invoices

import "time"

// Доменная модель (закрываем лишнее
type invoice struct { // тип тоже приватный
	ID     int
	amount int64
	paidAt *time.Time
}

// Публичный транспортный тип для API
type InvoiceDTO struct {
	ID     int    `json:"id"`
	Amount int64  `json:"amount"`
	PaidAt *int64 `json:"paid_at,omitempty"`
}

Как это работает: внутри пакета свободно оперируем приватными полями и инвариантами; наружу отдаём «чистый» DTO.

Встраивание (embedding) и видимость

При встраивании экспортируемые поля/методы «продвигаются» на уровень выше и доступны, если они экспортируемые.

type Base struct{ ID int } // экспортируемое поле

type Order struct {
	Base            // встраивание
	customer string // приватно: не видно извне
}

// В другом пакете: o.ID доступно, o.customer — нет.

Зачем: повторно используем общие поля (ID, Audit) без дублирования, при этом приватные детали остаются скрыты.

Тестирование и границы пакетов

  • Тесты в том же пакете (package x) видят приватные поля.
  • В «внешнем» стиле (package x_test) приватные поля не видны — тестируйте через публичное API.

Это мотивирует проектировать удобные публичные методы и не «протаскивать» приватные детали наружу ради тестов.

Экспорт методов на приватных типах

Можно объявить экспортируемый метод на приватном типе, но тип всё равно нельзя использовать извне, так как его имя недоступно.

type secret struct{}

func (secret) Do() {} // метод Do экспортируемый по имени, но тип secret — нет

Практически это мало полезно: извне не получится создать secret.

Нейминг и стиль

Следуйте стандартам Go:

  • Экспортируемые имена — понятные, без префиксов Get/Set, если не требуется.
  • Акронимы полностью капсом: HTTPServer, URL, ID (а не HttpServer, Id).

Производительность

Экспорт/приватность не влияют на скорость доступа на рантайме. Это исключительно правило компилятора.

Мини-кейсы

1) Конфиг сервиса

package cfg

import "fmt"

type Config struct { // только через конструктор
	host string
	port int
}

func New(host string, port int) (*Config, error) {
	if host == "" || port <= 0 {
		return nil, fmt.Errorf("bad config")
	}

	return &Config{host: host, port: port}, nil
}

func (c *Config) Addr() string { return fmt.Sprintf("%s:%d", c.host, c.port) }

Зачем: не позволяем собрать невалидный конфиг, наружу отдаём только безопасный Addr().

2) Модель для БД и для API

package users

import "time"

type User struct {
	ID    int
	Name  string
	Email string

	// технические поля, скрытые наружу
	createdAt time.Time
	updatedAt time.Time
}

type UserDTO struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

func ToDTO(u User) UserDTO {
	return UserDTO{ID: u.ID, Name: u.Name, Email: u.Email}
}

Как работает: доменная модель может меняться, API — стабильное. Приватные даты не «протекают» в ответ.

3) Безопасные деньги

package money

type Amount struct{ cents int64 } // скрываем единицу хранения

func NewAmount(rubles string) (Amount, error) {
	// парсим строку, нормализуем, запрещаем NaN/∞
	c, err := parseRubles(rubles)

	if err != nil {
		return Amount{}, err
	}

	return Amount{cents: c}, nil
}

func (a Amount) String() string { return formatRubles(a.cents) }

Зачем: снаружи нельзя сделать Amount{cents: -1} или хранить дроби с плавающей точкой.

4) Иммутабельность по договорённости

package geo

import "errors"

type Point struct{ X, Y float64 } // открыто — для DTO

type polygon struct{ points []Point } // закрыто — для инвариантов

func NewPolygon(ps []Point) (polygon, error) {
	if len(ps) < 3 {
		return polygon{}, errors.New("need >=3 points")
	}

	// можно проверить самопересечения и т.д.
	return polygon{points: append([]Point(nil), ps...)}, nil // копируем срез!
}

Пояснение: закрытый тип + копирование среза защищают от внешних изменений исходного массива.

Итог

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

В Go нет отдельного «специального синтаксиса» для экспорта полей. Всё работает очень просто:

  • Заглавная буква в имени поля (или метода, функции, типа, константы, переменной) → символ экспортируемый (виден из других пакетов).
  • Строчная буква → символ приватный (виден только внутри текущего пакета).

Это общее правило для всего Go, не только для структур.

Примеры:

type User struct {
	ID    int    // экспортируемое поле, доступно извне
	Name  string // тоже экспортируемое
	email string // приватное, видно только внутри пакета
}

// Экспортируемая функция (с заглавной буквы)
func NewUser(id int, name, email string) *User {
	return &User{ID: id, Name: name, email: email}
}

// Приватная функция (со строчной буквы)
func validateEmail(email string) bool {
	return strings.Contains(email, "@")
}

Здесь нет никаких модификаторов (public, private, protected), как в Java или C#. Всё регулируется только первой буквой имени.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845