Зарегистрируйтесь, чтобы продолжить обучение

Поля структуры и их типы Структуры в 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) & lt; 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#. Всё регулируется только первой буквой имени.


Самостоятельная работа

Спроектируем два пакета и воспользуемся ими. В одном пакете описываем сотрудников, в другом — расчёт зарплаты.

Пакет employees

Опишите структуру Employee со следующими полями:

  • Имя — строка
  • Должность — строка
  • Базовая ставка — число с плавающей точкой (приватное поле)
  • Навыки — список строк

Добавьте методы для работы с приватным полем:

  • SetBaseSalary(amount float64) bool — устанавливает базовую ставку (> 0), возвращает true, если значение принято;
  • BaseSalary() float64 — возвращает текущую базовую ставку.
Пример решения
package employees

type Employee struct {
    Name       string
    Position   string
    baseSalary float64
    Skills     []string
}

func (e *Employee) SetBaseSalary(amount float64) bool {
    if amount <= 0 {
        return false
    }
    e.baseSalary = amount
    return true
}

func (e Employee) BaseSalary() float64 { return e.baseSalary }

Пакет payroll

Реализуйте расчёт заработной платы на основании сотрудников из пакета employees:

  • Функция CalcGross(e employees.Employee, bonusPct float64) float64 — считает «грязную» зарплату как базу + бонус в процентах;
  • Функция CalcNet(gross float64, taxPct float64) float64 — считает «чистую» зарплату после удержания налога в процентах.

Обратите внимание: пакет payroll должен импортировать employees и использовать только публичные части API (BaseSalary()), без доступа к приватным полям.

Пример решения
package payroll

import "your/module/employees"

func CalcGross(e employees.Employee, bonusPct float64) float64 {
    base := e.BaseSalary()
    return base + base*bonusPct/100
}

func CalcNet(gross float64, taxPct float64) float64 {
    return gross * (1 - taxPct/100)
}

Использование в main

Импортируйте оба пакета, создайте не меньше трёх сотрудников, задайте им базовые ставки и посчитайте «грязную» и «чистую» зарплаты. Попробуйте обратиться к приватному полю baseSalary напрямую — убедитесь, что оно недоступно (ошибка компиляции; затем закомментируйте строку).

Пример решения
package main

import (
    "fmt"
    em "your/module/employees"
    pr "your/module/payroll"
)

func main() {
    team := []em.Employee{
        {Name: "Анна", Position: "Backend", Skills: []string{"Go", "SQL"}},
        {Name: "Павел", Position: "Data", Skills: []string{"Python", "ETL"}},
        {Name: "Ирина", Position: "QA", Skills: []string{"API testing"}},
    }

    if !team[0].SetBaseSalary(240000) {
        fmt.Println("ошибка: некорректная базовая ставка для", team[0].Name)
    }
    if !team[1].SetBaseSalary(210000) {
        fmt.Println("ошибка: некорректная базовая ставка для", team[1].Name)
    }
    if !team[2].SetBaseSalary(180000) {
        fmt.Println("ошибка: некорректная базовая ставка для", team[2].Name)
    }

    bonusPct := 10.0
    if bonusPct < 0 {
        fmt.Println("предупреждение: отрицательный бонус, устанавливаем 0%")
        bonusPct = 0
    }
    taxPct := 13.0
    if taxPct < 0 || taxPct > 100 {
        fmt.Println("предупреждение: некорректный налог, устанавливаем 13%")
        taxPct = 13
    }

    for _, e := range team {
        gross := pr.CalcGross(e, bonusPct)
        net := pr.CalcNet(gross, taxPct)
        fmt.Printf("%s (%s): gross=%.2f, net=%.2f\n", e.Name, e.Position, gross, net)
        // _ = e.baseSalary // недоступно: приватное поле в пакете employees
    }
}

Дополнительные материалы

  1. Effective Go — Names (экспортируемость)
  2. Go Spec — Exported identifiers
  3. Go Spec — Struct types

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff