{}const=>[]async()letfn</>var
РазработкаBackend

Структуры в Go: объекты без классической ООП-драмы

Разбираемся, как работают структуры в Go: поля, методы, указатели, конструкторы, композиция и embedding. Почему в Go нет привычных классов, но объектный подход никуда не исчез.

К

Кодик

Автор

7 мин чтения

Если вы пришли в Go после Java, C# или C++, первое знакомство со структурами может вызвать лёгкое недоумение.

Где класс?

Где extends?

Где public, private, protected?

Где абстрактный фабричный менеджер фабрик, без которого невозможно вывести "Hello World"?

Go смотрит на всё это примерно так:

А может… просто данные и методы?

И достаёт struct.

Что такое struct в Go?

Структура — это тип, который объединяет несколько связанных значений.

Например, у нас есть пользователь:

type User struct {
    Name  string
    Age   int
    Email string
}

Теперь можно создать значение этого типа:

user := User{
    Name:  "Alex",
    Age:   24,
    Email: "alex@example.com",
}

И обращаться к полям:

fmt.Println(user.Name)
fmt.Println(user.Age)

Выглядит подозрительно просто.

Так и задумано.

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

🔥 100 000+ учеников уже с нами

Устал читать теорию?
Пора кодить!

Кодик — приложение, где ты учишься программировать через практику. AI-наставник, интерактивные уроки, реальные проекты.

🤖 AI 24/7
🎓 Сертификаты
💰 Бесплатно
🚀 Начать учиться
Присоединились сегодня

Создание структуры

Полное заполнение

user := User{
    Name:  "Max",
    Age:   21,
    Email: "max@example.com",
}

Это самый читаемый вариант. Особенно если структура большая.

Можно заполнить только часть полей

user := User{
    Name: "Max",
}

Остальные получат нулевые значения.

Для string:

""

Для int:

0

Для bool:

false

То есть Go не заставляет вас срочно писать конструктор только потому, что иначе Вселенная развалится.

А где конструктор?

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

Специального синтаксиса конструктора нет. Вообще.

Если нужен — пишем обычную функцию:

func NewUser(name string, email string) User {
    return User{
        Name:  name,
        Email: email,
    }
}

Используем:

user := NewUser("Alex", "alex@example.com")

Часто функции-конструкторы в Go называют через New:

NewUser()
NewServer()
NewClient()
NewDatabase()

Но это соглашение, а не магия языка.

Go не вызывает тайно пять скрытых методов за вашей спиной.

Методы у структур

Хорошо. Данные есть.

Но объект без поведения — это пока просто красивый пакет переменных.

Добавим метод:

type User struct {
    Name string
    Age  int
}

func (u User) Greet() {
    fmt.Println("Привет, меня зовут", u.Name)
}

Теперь:

user := User{Name: "Alex"}
user.Greet()

Получаем:

Привет, меня зовут Alex

Вот вам объектное поведение.

Без ключевого слова class.

Никто не пострадал.

Что такое receiver?

Вот эта часть:

(u User)

называется receiver.

Она показывает, к какому типу относится метод.

Можно мысленно читать:

func (u User) Greet()

как: функция Greet, принадлежащая типу User.

По смыслу похоже на:

user.greet();

из мира Java.

Но технически Go не строит вокруг этого классическую систему классов.

Изменяем структуру: value receiver против pointer receiver

Допустим, хотим сделать пользователя старше:

func (u User) Birthday() {
    u.Age++
}

Пишем:

user := User{
    Name: "Alex",
    Age:  20,
}
user.Birthday()
fmt.Println(user.Age)

И получаем:

20

Начинающий Go-разработчик:

ЧЕГО

Причина проста.

u User получает копию структуры.

Вы изменили копию. Она честно изменилась. А затем исчезла.

Спасибо за участие.

Чтобы менять оригинал, используем указатель:

func (u *User) Birthday() {
    u.Age++
}

Теперь:

user.Birthday()
fmt.Println(user.Age)

Получим:

21

Простое правило

Если метод должен изменять структуру, обычно используйте:

func (u *User)

Если метод только читает данные, возможен:

func (u User)

Хотя в реальных проектах выбор может зависеть ещё и от размера структуры и единообразия API.

Почему нам не приходится писать (*u).Age

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

Можно написать:

u.Age++

вместо:

(*u).Age++

Язык понимает, что вы имели в виду.

Редкий момент, когда компилятор говорит:

Ладно, брат, тут я сам.

Указатель на структуру

Структуру можно создать через &:

user := &User{
    Name: "Alex",
    Age:  20,
}

Теперь user имеет тип:

*User

Можно проверить:

fmt.Printf("%T\n", user)

И получить:

*main.User

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

Поля структур и экспорт

В Go нет привычных:

public
private
protected

Вместо этого есть очень простое правило.

Если имя начинается с заглавной буквы, оно экспортируется:

type User struct {
    Name string
}

Name доступно из других пакетов.

А:

type User struct {
    name string
}

name доступно только внутри текущего пакета.

Да. Модификатор доступа буквально определяется регистром первой буквы.

Java-разработчик в этот момент где-то роняет public static final.

Вложенные структуры

Структуры можно помещать друг в друга.

type Address struct {
    City    string
    Country string
}

type User struct {
    Name    string
    Address Address
}

Создание:

user := User{
    Name: "Alex",
    Address: Address{
        City:    "Moscow",
        Country: "Russia",
    },
}

Обращение:

fmt.Println(user.Address.City)

Это уже позволяет собирать довольно сложные модели.

Композиция вместо наследования

Вот тут Go окончательно говорит классической ООП:

Мы решили остаться друзьями.

Допустим, есть логгер:

type Logger struct{}

func (Logger) Log(message string) {
    fmt.Println("[LOG]", message)
}

И сервер:

type Server struct {
    Logger Logger
}

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

server := Server{}
server.Logger.Log("Server started")

Вместо:

Server extends Logger

мы просто говорим: Server содержит Logger.

Это и есть композиция.

И довольно часто она приводит к более понятной архитектуре, чем глубокое наследование.

Потому что:

PremiumAdminUser extends AdminUser extends User extends Person extends Human extends Mammal

— это уже не архитектура.

Это генеалогическое древо.

Embedded fields: когда можно ещё короче

Go позволяет встроить один тип в другой:

type Logger struct{}

func (Logger) Log(message string) {
    fmt.Println(message)
}

type Server struct {
    Logger
}

Теперь можно написать:

server := Server{}
server.Log("Started")

Вместо:

server.Logger.Log("Started")

Такой механизм называется embedding.

Он немного напоминает наследование, но это не классическое наследование.

Server не становится Logger. Он содержит Logger и получает удобный доступ к его полям и методам.

Несколько встроенных структур

type Logger struct{}

func (Logger) Log(message string) {
    fmt.Println(message)
}

type Metrics struct{}

func (Metrics) Track() {
    fmt.Println("Tracking metrics")
}

type Server struct {
    Logger
    Metrics
}

Теперь:

server := Server{}
server.Log("Started")
server.Track()

Получается своеобразный конструктор LEGO.

Нужна функциональность — добавили компонент. Не нужна — не добавили.

Без наследования от AbstractEnterpriseBaseServerFactoryV2.

Структуры и интерфейсы — вот где начинается настоящая магия Go

Допустим, есть интерфейс:

type Speaker interface {
    Speak()
}

И структура:

type Dog struct {
    Name string
}

Добавляем метод:

func (d Dog) Speak() {
    fmt.Println("Woof!")
}

Всё.

Dog теперь удовлетворяет интерфейсу Speaker.

Не нужно писать:

implements Speaker

Go просто смотрит:

Метод есть? Есть. Ну и отлично.

Используем:

func MakeSpeak(s Speaker) {
    s.Speak()
}

dog := Dog{Name: "Bob"}
MakeSpeak(dog)

Вот почему интерфейсы и структуры в Go настолько хорошо работают вместе.

Связи между типами получаются слабее, а код — гибче.

Пример из реального проекта

Представим API интернет-магазина.

Есть товар:

type Product struct {
    ID    int
    Name  string
    Price float64
}

Добавим метод:

func (p Product) FinalPrice(discount float64) float64 {
    return p.Price * (1 - discount)
}

Используем:

product := Product{
    ID:    1,
    Name:  "Mechanical Keyboard",
    Price: 100,
}

price := product.FinalPrice(0.15)
fmt.Println(price)

Получим:

85

И никакой отдельный ProductServiceFactorySingleton для вычисления цены нам пока не понадобился.

Красота.

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

Иногда отдельный тип создавать вообще не хочется.

user := struct {
    Name string
    Age  int
}{
    Name: "Alex",
    Age:  23,
}

Это анонимная структура.

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

Структуры и JSON

Очень частый сценарий в backend-разработке на Go:

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

Вот эти части:

`json:"id"`

называются тегами структуры.

Они помогают пакетам вроде encoding/json понимать, как преобразовывать поля.

user := User{
    ID:    1,
    Name:  "Alex",
    Email: "alex@example.com",
}

data, err := json.Marshal(user)
if err != nil {
    log.Fatal(err)
}

fmt.Println(string(data))

Получим примерно:

{"id":1,"name":"Alex","email":"alex@example.com"}

А вот здесь уже начинается Go-бэкенд в его естественной среде обитания.

struct. JSON. API.

И где-то неподалёку обязательно сидит:

if err != nil

omitempty

Можно попросить JSON не выводить пустые значения:

type User struct {
    Name   string `json:"name"`
    Avatar string `json:"avatar,omitempty"`
}

Если:

Avatar == ""

поле можно не включать в JSON.

Очень удобно для API.

Сравнение структур

Некоторые структуры в Go можно сравнивать напрямую:

type Point struct {
    X int
    Y int
}

a := Point{X: 10, Y: 20}
b := Point{X: 10, Y: 20}

fmt.Println(a == b)

Получим:

true

Но только если все поля структуры сами являются сравнимыми.

Например, структура со slice:

type User struct {
    Tags []string
}

уже не сравнивается обычным ==.

А ООП в Go вообще есть?

Это зависит от того, насколько священной для вас является формулировка из учебника.

В Go есть:

  • типы;

  • структуры;

  • методы;

  • интерфейсы;

  • инкапсуляция через пакеты;

  • композиция;

  • полиморфизм через интерфейсы.

Но нет классического наследования классов.

И нет классов в привычном Java/C#-смысле.

Поэтому правильнее сказать: Go позволяет использовать многие объектно-ориентированные идеи, но делает это без классической модели ООП.

И именно поэтому код часто получается довольно прямолинейным.

Где потренироваться со структурами в Go

Прочитать про struct — полезно.

Но настоящий момент истины наступает примерно здесь:

func (u User)

против:

func (u *User)

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

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

Если изучаете Go, особенно полезно не просто читать синтаксис, а самостоятельно собирать небольшие структуры: пользователя, товар, персонажа игры, сервер, банковский счёт.

А ещё у нас есть Telegram-сообщество Кодика, где выходят короткие полезные посты про программирование, языки, инструменты и разработку. Удобный вариант регулярно повторять материал и узнавать новые штуки без очередной лекции на три часа.

Мини-практика

Попробуйте самостоятельно написать:

type Player struct {
    Name   string
    Health int
    Level  int
}

Добавьте методы:

TakeDamage()
Heal()
LevelUp()
IsAlive()

Например:

func (p *Player) TakeDamage(damage int) {
    p.Health -= damage

    if p.Health < 0 {
        p.Health = 0
    }
}

Метод проверки:

func (p Player) IsAlive() bool {
    return p.Health > 0
}

И тест:

player := Player{
    Name:   "Neo",
    Health: 100,
    Level:  1,
}

player.TakeDamage(40)

fmt.Println(player.Health)
fmt.Println(player.IsAlive())

Если всё работает — поздравляем.

Вы только что создали объект с состоянием и поведением.

И ни один класс при этом не был создан.

🎯Хватит откладывать

Понравилась статья?
Пора применять на практике!

В Кодик ты не просто читаешь — ты сразу пишешь код. Теория + практика = реальный скилл.

Мгновенная практика
🧠AI объяснит код
🏆Сертификат

Без регистрации • Без карты