Go. Структуры данных
Методы структур:
определение и использование

Введение
Вы уже знаете, как создавать структуры для хранения данных. Например, структура User с полями Name и Email. Но что если вам нужно не просто хранить данные, а выполнять с ними действия? Отправлять приветственное письмо пользователю, валидировать его email, форматировать вывод информации? Можно написать отдельные функции, которые принимают User как параметр, но в Go есть более элегантный способ — методы.
Методы — это функции, которые привязаны к конкретному типу. Вместо вызова SendEmail(user) вы пишете user.SendEmail(). Это делает код более читаемым и логически связывает функциональность с данными.
В этой лекции мы разберём, как определять методы для структур, в чём разница между методами с value receiver и pointer receiver, и когда использовать каждый из них.
Содержание
Что такое методы?
Метод — это функция с дополнительным аргументом-получателем (receiver), который указывается между ключевым словом func и именем функции. Получатель связывает функцию с определённым типом, превращая её в метод этого типа.
В других языках программирования (например, в Java или Python) методы — это функции, объявленные внутри класса. В Go нет классов, но есть методы, которые привязываются к типам через receiver.
type Person struct {
    Name string
    Age  int
}

// Это метод структуры Person
func (p Person) Greet() string {
    return "Hello, my name is " + p.Name
}
В этом примере (p Person) — это receiver. Он говорит, что функция Greet является методом типа Person. Внутри метода мы можем обращаться к полям структуры через переменную p.
Определение методов
Базовый синтаксис
Синтаксис определения метода выглядит так:
func (receiver Type) MethodName(parameters) returnType {
    // тело метода
}
Давайте рассмотрим пример с банковским счётом:
type Account struct {
    Owner   string
    Balance float64
}

// Метод для получения информации о счёте
func (a Account) Info() string {
    return fmt.Sprintf("Account owner: %s, Balance: %.2f", a.Owner, a.Balance)
}

// Метод для проверки, достаточно ли средств
func (a Account) HasEnoughMoney(amount float64) bool {
    return a.Balance >= amount
}
В этом примере мы определили два метода для структуры Account. Receiver a — это просто переменная, через которую мы обращаемся к полям структуры внутри метода. Имя receiver принято делать коротким (обычно первая буква названия типа в нижнем регистре).
Вызов методов
Методы вызываются через точку после переменной соответствующего типа:
func main() {
    acc := Account{
        Owner:   "Alice",
        Balance: 1000.0,
    }
    
    fmt.Println(acc.Info())                    // ← Account owner: Alice, Balance: 1000.00
    fmt.Println(acc.HasEnoughMoney(500.0))     // ← true
    fmt.Println(acc.HasEnoughMoney(1500.0))    // ← false
}
Синтаксис вызова метода интуитивно понятен: переменная. метод (аргументы). Это делает код более читаемым по сравнению с передачей структуры в обычную функцию.
Value receiver vs Pointer receiver
Одна из важнейших концепций при работе с методами — это выбор между value receiver и pointer receiver. От этого выбора зависит, будет ли метод работать с копией структуры или с оригиналом.
Value receiver
Когда вы определяете метод с value receiver, Go создаёт копию структуры при каждом вызове метода:
type Counter struct {
    Value int
}

// Метод с value receiver
func (c Counter) Increment() {
    c.Value++  // ← Изменяется только копия!
}

func main() {
    counter := Counter{Value: 0}
    counter.Increment()
    fmt.Println(counter.Value)  // ← Выведет 0, не 1!
}
В этом примере метод Increment получает копию структуры Counter. Изменение c.Value++ влияет только на эту копию, а оригинальная структура counter остаётся неизменной.
Value receiver подходит для методов, которые только читают данные структуры, но не изменяют её:
type Rectangle struct {
    Width  float64
    Height float64
}

// Value receiver подходит, так как мы только вычисляем значение
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}
Pointer receiver
Если вам нужно изменить структуру внутри метода, используйте pointer receiver:
type Counter struct {
    Value int
}

// Метод с pointer receiver
func (c *Counter) Increment() {
    c.Value++  // ← Изменяется оригинал
}

func main() {
    counter := Counter{Value: 0}
    counter.Increment()
    fmt.Println(counter.Value)  // ← Выведет 1
}
Обратите внимание на звёздочку перед типом в receiver: (c *Counter). Это указывает, что метод получает указатель на структуру, а не её копию. Теперь изменения внутри метода влияют на оригинальную структуру.
type Account struct {
    Balance float64
}

func (a *Account) Deposit(amount float64) {
    a.Balance += amount  // ← Изменяет оригинал
}

func (a Account) GetBalance() float64 {
    return a.Balance  // ← Только читает
}
Метод Deposit использует pointer receiver, потому что изменяет баланс. Метод GetBalance использует value receiver, потому что только читает данные.
Автоматическая конвертация
Go автоматически конвертирует между значениями и указателями при вызове методов в обе стороны:
type Counter struct {
    Value int
}

func (c *Counter) Increment() {
    c.Value++
}

func (c Counter) GetValue() int {
    return c.Value
}

func main() {
    // Значение → указатель (для pointer receiver)
    counter1 := Counter{Value: 5}
    counter1.Increment()  // ← Go автоматически преобразует в (&counter1).Increment()
    
    // Указатель → значение (для value receiver)
    counter2 := &Counter{Value: 10}
    val := counter2.GetValue()  // ← Go автоматически разыменовывает в (*counter2).GetValue()
    fmt.Println(val)  // ← 10
}
Когда у вас есть значение, а метод ожидает pointer receiver — Go берёт адрес автоматически. Когда у вас есть указатель, а метод ожидает value receiver — Go автоматически разыменовывает указатель.
Автоматическая конвертация работает только для переменных. Если вы вызываете метод на литерале или результате функции, Go не сможет взять адрес.
func GetCounter() Counter {
    return Counter{Value: 5}
}

func main() {
    // Это не скомпилируется!
    // GetCounter().Increment()  // ← Ошибка: cannot take the address of GetCounter()
    
    // Нужно сначала сохранить в переменную
    c := GetCounter()
    c.Increment()  // ← Теперь работает
}
Когда использовать pointer receiver?
Есть три основных случая, когда следует использовать pointer receiver:
Когда метод изменяет структуру
Если метод должен модифицировать поля структуры, используйте pointer receiver:
type Temperature struct {
    Celsius float64
}

func (t *Temperature) SetFahrenheit(f float64) {
    t.Celsius = (f - 32) * 5 / 9
}
Когда структура большая
Если структура содержит много полей или большие массивы, копирование при каждом вызове метода будет неэффективным. Используйте pointer receiver, чтобы избежать копирования:
type LargeData struct {
    Data [1000000]int
    // ... много других полей
}

// Pointer receiver избегает копирования миллиона чисел
func (ld *LargeData) Process() {
    // обработка данных
}
Когда хотя бы один метод использует pointer receiver
Если для типа определены методы, и хотя бы один из них использует pointer receiver, для консистентности лучше использовать pointer receiver во всех методах этого типа:
type User struct {
    Name  string
    Email string
    Age   int
}

func (u *User) SetEmail(email string) {
    u.Email = email  // ← Изменяет структуру
}

// Для консистентности используем pointer receiver, хотя метод только читает
func (u *User) GetInfo() string {
    return fmt.Sprintf("%s (%s), age: %d", u.Name, u.Email, u.Age)
}
Это правило помогает избежать путаницы и делает API типа более предсказуемым. Если разработчик видит, что некоторые методы изменяют объект, он ожидает, что все методы работают с одним и тем же объектом, а не с его копиями.
Инкапсуляция через методы
Одно из ключевых преимуществ методов — возможность реализовать инкапсуляцию данных. В Go поля структуры могут быть неэкспортируемыми (начинаться с маленькой буквы), а методы — экспортируемыми (начинаться с большой буквы). Это позволяет контролировать, как внешний код работает с данными.
type account struct {
    balance float64  // ← Неэкспортируемое поле
}

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

// Экспортируемый метод
func (a account) Balance() float64 {
    return a.balance
}
Поле balance неэкспортируемое — код из других пакетов не может напрямую изменить баланс. Все операции проходят через экспортируемые методы, которые могут выполнять валидацию. Это защищает данные от некорректных изменений.
Методы для встроенных типов
В Go можно определять методы не только для структур, но и для любых пользовательских типов, основанных на встроенных типах:
type Celsius float64
type Fahrenheit float64

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (f Fahrenheit) ToCelsius() Celsius {
    return Celsius((f - 32) * 5 / 9)
}

func main() {
    temp := Celsius(20.0)
    fmt.Printf("%.1f°C = %.1f°F\n", temp, temp.ToFahrenheit())  // ← 20.0°C = 68.0°F
}
Это позволяет добавить методы к простым типам, делая код более выразительным. Однако есть ограничение: вы можете определять методы только для типов, объявленных в том же пакете.
// Это не скомпилируется!
// func (s string) Reverse() string {  // ← Ошибка: cannot define new methods on non-local type string
//     // ...
// }

// Нужно создать свой тип
type MyString string

func (s MyString) Reverse() string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}
Методы и nil receiver
В Go метод с pointer receiver может быть вызван даже на nil указателе. Это может быть полезно для реализации безопасных методов, которые корректно обрабатывают отсутствие объекта:
type Tree struct {
    Value int
    Left  *Tree
    Right *Tree
}

// Метод работает корректно даже для nil
func (t *Tree) Sum() int {
    if t == nil {
        return 0
    }
    return t.Value + t.Left.Sum() + t.Right.Sum()
}

func main() {
    tree := &Tree{
        Value: 1,
        Left:  &Tree{Value: 2},
        Right: nil,  // ← Right равен nil
    }
    
    fmt.Println(tree.Sum())  // ← 3 (1 + 2 + 0)
}
В этом примере метод Sum рекурсивно вызывается для левого и правого поддеревьев. Когда мы вызываем t.Right.Sum(), где Right равен nil, метод всё равно выполняется и корректно возвращает 0.
Это элегантный способ работы с рекурсивными структурами данных, но требует осторожности: нужно всегда проверять receiver на nil в начале метода.
Заключение
Методы в Go — это мощный инструмент для организации кода и добавления поведения к типам данных. Они делают код более выразительным и понятным, позволяя работать с данными через их интерфейс, а не через набор разрозненных функций.
Ключевые моменты:
  • Метод 
    Это функция с receiver, связывающим её с конкретным типом
  • Value receiver
    Передаёт копию структуры, pointer receiver — указатель на оригинал
  • Используйте pointer receiver
    Когда нужно изменить структуру, когда она большая, или для консистентности API
  • Go
    Автоматически конвертирует между значениями и указателями при вызове методов
  • Методы
    Можно определять для любых пользовательских типов, не только для структур
  • Методы с pointer receiver
    Могут корректно работать с nil receiver при правильной проверке
Что дальше?
Теперь вы готовы изучать следующие темы:
  • Вложенные структуры и встраивание (composition)
  • Интерфейсы: объявление и реализация, полиморфизм
  • Интерфейсы: внутренее устройство
Поздравляю, вы только что освоили методы структур в Go!
Это знание поможет вам создавать более выразительный и организованный код, инкапсулируя логику работы с данными.