Struct и методы в Go: первый объект без ООП-магии

В Go нет классов в привычном смысле, но есть struct и методы. Этого хватает, чтобы описывать пользователя, заказ, платеж, HTTP-ответ или строку из базы данных. В этом уроке соберем данные заказа в struct, добавим метод Total, разберем receiver и поймем, когда нужен pointer receiver

Главная мысль: struct хранит данные, метод описывает действие рядом с этими данными. Без наследования, без скрытых конструкторов и без тяжелой терминологии

Что получится в конце

Мы напишем такой код:

package main

import "fmt"

type Order struct {
    ID       int
    Customer string
    Price    float64
    Quantity int
}

func (o Order) Total() float64 {
    return o.Price * float64(o.Quantity)
}

func main() {
    order := Order{
        ID:       101,
        Customer: "Алия",
        Price:    1200,
        Quantity: 2,
    }

    fmt.Println("Клиент:", order.Customer)
    fmt.Println("Итог:", order.Total())
}

Запуск:

go run .

Ожидаемый вывод:

Клиент: Алия
Итог: 2400

Создаем проект

mkdir go-structs
cd go-structs
go mod init example.com/go-structs

Создайте main.go и вставьте код из предыдущего блока. Если запуск работает, идем разбирать его спокойно

Что такое struct

Объявление:

type Order struct {
    ID       int
    Customer string
    Price    float64
    Quantity int
}

создает новый тип Order. У него четыре поля: ID, Customer, Price, Quantity

Можно думать о struct как о собственной форме записи данных. Вместо отдельных переменных:

customer := "Алия"
price := 1200.0
quantity := 2

мы собираем связанные значения в одну сущность:

order := Order{
    Customer: "Алия",
    Price:    1200,
    Quantity: 2,
}

Так код легче передавать в функции, возвращать из функций и хранить в срезах

Composite literal: как создавать значение struct

Самый читаемый способ создать struct:

order := Order{
    ID:       101,
    Customer: "Алия",
    Price:    1200,
    Quantity: 2,
}

Это называется composite literal. Мы указываем имена полей и значения. Такой вариант лучше для учебного и прикладного кода, потому что порядок полей не нужно держать в голове

Есть короткий вариант:

order := Order{101, "Алия", 1200, 2}

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

Метод и receiver

Метод:

func (o Order) Total() float64 {
    return o.Price * float64(o.Quantity)
}

похож на обычную функцию, но между func и именем метода есть receiver:

(o Order)

Это значит: метод Total относится к типу Order, а внутри метода текущий заказ доступен через переменную o

Вызов выглядит так:

order.Total()

То есть мы читаем его как действие у конкретного значения заказа

Чем метод отличается от обычной функции

Можно было написать обычную функцию:

func total(order Order) float64 {
    return order.Price * float64(order.Quantity)
}

И вызвать:

fmt.Println(total(order))

Оба варианта рабочие. Метод удобен, когда действие естественно принадлежит типу. Order.Total() читается лучше, чем total(order), потому что сумма заказа выглядит как поведение заказа

Но не каждую функцию нужно превращать в метод. Если функция просто обрабатывает несколько независимых значений, обычная функция может быть честнее

Value receiver и pointer receiver

В методе Total receiver такой:

func (o Order) Total() float64

Это value receiver. Метод получает копию значения Order. Для чтения данных это отлично

Если метод должен изменить заказ, нужен pointer receiver:

func (o *Order) ApplyDiscount(percent float64) {
    discount := o.Price * percent / 100
    o.Price = o.Price - discount
}

Теперь добавим в пример:

func (o *Order) ApplyDiscount(percent float64) {
    discount := o.Price * percent / 100
    o.Price = o.Price - discount
}

И вызовем:

order.ApplyDiscount(10)
fmt.Println("Итог со скидкой:", order.Total())

Go позволяет писать order.ApplyDiscount(10) без ручного &order, если переменная адресуемая. Но смысл остается: метод меняет исходный объект, поэтому работает через указатель

Полный пример со скидкой

package main

import "fmt"

type Order struct {
    ID       int
    Customer string
    Price    float64
    Quantity int
}

func (o Order) Total() float64 {
    return o.Price * float64(o.Quantity)
}

func (o *Order) ApplyDiscount(percent float64) {
    discount := o.Price * percent / 100
    o.Price = o.Price - discount
}

func main() {
    order := Order{
        ID:       101,
        Customer: "Алия",
        Price:    1200,
        Quantity: 2,
    }

    fmt.Println("До скидки:", order.Total())
    order.ApplyDiscount(10)
    fmt.Println("После скидки:", order.Total())
}

Вывод:

До скидки: 2400
После скидки: 2160

Когда использовать struct

Используйте struct, когда несколько полей описывают одну сущность: пользователь, заказ, статья, настройка, ошибка ответа, событие

Не используйте struct только ради красоты, если у вас одно значение и нет понятной сущности. Go хорошо относится к простому коду. Иногда отдельная функция с двумя параметрами читается лучше, чем искусственный тип

Частые ошибки

unknown field Customer. Проверьте имя поля в объявлении struct. Go чувствителен к регистру: Customer и customer это разные имена

cannot assign to o.Price. Возможно, метод использует value receiver, а вы пытаетесь изменить поле. Для изменения нужен pointer receiver: (o *Order)

field is not exported. Если поле начинается с маленькой буквы, оно доступно только внутри своего пакета. Для JSON, внешних пакетов и некоторых библиотек это важно

struct literal uses unkeyed fields. Это не всегда ошибка компиляции, но часто плохой знак в прикладном коде. Лучше использовать именованные поля

Что может быть еще интересно по этой теме

Go правда без классов? Да, в Go нет классов и наследования как в Java или C Sharp. Но struct, методы и interfaces закрывают большинство повседневных задач

Нужно ли всегда использовать pointer receiver? Нет. Если метод только читает маленькое значение, value receiver нормален. Если метод меняет объект или структура большая, чаще выбирают pointer receiver

Можно ли добавить метод к int или string? К встроенному int напрямую нельзя, но можно создать свой тип на основе int и добавить метод к нему

Почему поля пишут с большой буквы? Большая буква экспортирует имя из пакета. В одном пакете можно использовать маленькие поля, но для примеров и будущего JSON часто удобнее видеть экспортируемые поля

Что открыть дальше

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x