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

Методы у структур. Инкапсуляция и экспортируемость Структуры в Go

Введение

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

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

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

Первые шаги: методы как функции у объекта

В самом начале у нас есть простая структура User, которая хранит имя и email пользователя:

type User struct {
    Name  string
    Email string
}

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

// Обычная функция, принимает User как аргумент
func Greet(u User) string {
    return "Привет, " + u.Name
}

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

user := User{Name: "Иван", Email: "ivan@example.com"}
fmt.Println(Greet(user)) // "Привет, Иван"

Вроде все работает, но получается не очень удобно: мы должны помнить, что у нас есть какая-то функция Greet, и каждый раз явно передавать туда user.

В Go есть более естественный способ — сделать метод у самой структуры User:

// Метод у структуры User. Обратите внимание на (u User) перед именем.
// Это "получатель" (receiver), который связывает функцию со структурой.
func (u User) Greet() string {
    return "Привет, " + u.Name
}

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

user := User{Name: "Иван", Email: "ivan@example.com"}
fmt.Println(user.Greet()) // "Привет, Иван"

Как выглядит синтаксис метода в Go

В Go метод — это не отдельная сущность языка, как, например, в Java или C#. Здесь нет ключевого слова method или конструкции class. Все устроено проще: метод объявляется тем же ключевым словом func, что и обычная функция.

Разница только в одном: у метода есть получатель (receiver). Он пишется в круглых скобках перед именем функции и указывает, к какому типу этот метод относится.

Общий шаблон:

func (r ReceiverType) MethodName(params...) returnTypes {
// тело метода
}

или, если метод должен изменять данные:

func (r *ReceiverType) MethodName(params...) returnTypes {
// тело метода
}

Здесь:

  • r — имя переменной-получателя (обычно короткое, как u для User, a для Account).
  • ReceiverType — тип, к которому метод прикреплен (чаще всего структура).
  • MethodName — имя метода, которое затем можно вызвать у объекта.

Сравним функцию и метод

Функция:

func Greet(u User) string {
    return "Привет, " + u.Name
}

Метод:

func (u User) Greet() string {
    return "Привет, " + u.Name
}

Теперь можно вызвать так:

user := User{Name: "Иван"}
fmt.Println(user.Greet()) // вместо Greet(user)

Метод и функция внутри устроены одинаково. Главное отличие — в наличии (receiver) перед именем. Именно это связывает поведение с типом.

Поэтому можно сказать, что метод в Go — это обычная функция + получатель.

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

Методы, изменяющие состояние

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

type Account struct {
    Owner   string
    Balance float64
}

Если мы хотим реализовать метод для пополнения счета, нам нужно, чтобы он менял поле Balance. Для этого в Go используют указатель-получатель:

// Метод пополнения счета
// Обратите внимание: (a *Account), а не (a Account).
// Это значит, что метод работает с указателем и может изменять исходный объект.
func (a *Account) Deposit(amount float64) {
    a.Balance += amount
}

Теперь в коде:

acc := Account{Owner: "Мария", Balance: 100}
acc.Deposit(50) // баланс должен увеличиться на 50
fmt.Println(acc.Balance) // 150

Если бы мы написали (a Account) вместо (a *Account), то метод изменял бы только копию объекта, и исходный баланс остался бы прежним.

Проверка условий: инкапсуляция логики

Вариант «на коленке» для снятия денег выглядел бы так:

acc.Balance -= 500

Но это опасно: если на счету меньше денег, получится отрицательный баланс.

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

// Снятие денег со счета
// Если денег хватает — уменьшаем баланс и возвращаем true.
// Если нет — возвращаем false.
func (a *Account) Withdraw(amount float64) bool {
    if a.Balance >= amount {
        a.Balance -= amount
        return true
    }

    return false
}

Теперь в коде все аккуратно:

ok := acc.Withdraw(200)

if ok {
    fmt.Println("Снятие прошло успешно")
} else {
    fmt.Println("Недостаточно средств")
}

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

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

В Go нет ключевых слов public или private, как в других языках. Все решается очень просто:

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

Пример:

// Экспортируемый метод: его можно вызвать снаружи пакета
func (a *Account) Deposit(amount float64) {
    a.Balance += amount
}

// Приватный метод: доступен только внутри пакета, например для логов
func (a *Account) logTransaction(amount float64) {
    fmt.Println("Операция на сумму:", amount)
}

Таким образом, мы можем оставлять служебные методы «под капотом», а наружу показывать только удобный и безопасный интерфейс.

Более сложный пример: интернет-магазин

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

type Order struct {
    ID       int
    Customer string
    Items    []string
    Status   string
}

Что можно делать с заказом? Добавлять товары и менять статус. Запишем это методами:

// Добавить товар в заказ
func (o *Order) AddItem(item string) {
    o.Items = append(o.Items, item)
}

// Установить статус заказа

func (o *Order) SetStatus(status string) {
    o.Status = status
}

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

order := Order{ID: 101, Customer: "Иван"}

// Добавляем товары
order.AddItem("Ноутбук")
order.AddItem("Мышь")

// Меняем статус
order.SetStatus("Оплачен")
fmt.Println(order.Items) // [Ноутбук Мышь]
fmt.Println(order.Status) // Оплачен

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

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

На практике все зависит от роли структуры. Если это простая вспомогательная сущность вроде Point с координатами, у нее может быть всего один-два метода, например, для вычисления расстояния или сдвига точки. Если же структура отражает крупный объект из предметной области — например, Order в интернет-магазине или User в системе авторизации, — у нее могут быть десятки методов: добавление и удаление элементов, смена статуса, проверка условий, работа с историей.

Главное правило — методы должны быть осмысленными действиями именно для этой структуры. Если методов становится слишком много и они начинают «раздувать» код, это сигнал, что логику стоит разбить: вынести часть поведения в отдельные типы или интерфейсы.


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

Закрепим понимание получателей и инкапсуляции через простое упражнение.

Задачи:

  1. Опишите структуру Book с полями: Title string, Author string.
  2. Реализуйте метод Description() string с получателем по значению, который возвращает строку вида:

    "Название — Автор"
    
  3. Создайте пару книг и выведите описание для каждой.

Показать пример ответа
package main

import "fmt"

type Book struct {
    Title  string
    Author string
}

func (b Book) Description() string {
    return b.Title + " — " + b.Author
}

func main() {
    books := []Book{
        {Title: "Война и мир", Author: "Лев Толстой"},
        {Title: "Алые паруса", Author: "Александр Грин"},
    }
    for _, bk := range books {
        fmt.Println(bk.Description())
    }
}

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

  1. Tour of Go — Methods
  2. Effective Go — Methods
  3. Effective Go — Pointers vs. values
  4. Go Spec — Method declarations

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

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

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

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

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

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

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

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