Странные скобки перед именем функции в Go: разбор полетов

Онлайн-тренажер Python для начинающих

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

Начать курс

Странные скобки перед именем функции в Go: разбор полетов

Представьте: вы открываете чужой код на Go, и видите нечто, от чего у вас дергается глаз:

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

Стоп. Что это за (u User) перед именем функции? В Python для методов используется def greet(self):, в Java — public String greet() внутри класса. А тут — какие-то скобки, да еще и с типом внутри. Бред? Нет, гениально!

Эта конструкция — одна из самых мощных фич Go, которая часто ставит в тупик новичков. На самом деле, это receiver (получатель) — ключ к пониманию объектно-ориентированного программирования в Go.

В этой статье мы заглянем под капот Go-компилятора, разберем, как работают эти скобки, и почему без них ваш код будет выглядеть как из 90-х. Готовы? Поехали!

Что такое receiver и зачем он нужен?

В Go нет классов в классическом понимании. Но есть структуры (struct) и методы. Скобки перед именем функции — это и есть объявление метода для конкретного типа. Технически это называется receiver parameter.

Синтаксис прост:

func (receiver_name ReceiverType) MethodName(params) return_type {
    // тело метода
}

Здесь receiver_name — это переменная, через которую вы обращаетесь к полям структуры (как self в Python или this в Java). А ReceiverType — ваш тип (обычно структура).

Пример из жизни:

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

// Метод с receiver
func (u User) SayHello() {
    fmt.Printf("Привет! Я %s, мне %d лет.\n", u.Name, u.Age)
}

func main() {
    user := User{Name: "Алиса", Age: 25}
    user.SayHello() // Вывод: Привет! Я Алиса, мне 25 лет.
}

Обратите внимание: мы вызываем метод через точку, как в любом ООП-языке. Но под капотом Go делает хитрый трюк.

Value receiver vs Pointer receiver: битва титанов

В Go есть два типа receiver'ов: по значению (value receiver) и по указателю (pointer receiver). Разница колоссальная, и непонимание этого — частая причина багов.

Value receiver

Когда вы пишете func (u User) ..., Go копирует всю структуру. Метод работает с копией объекта. Изменения внутри метода не повлияют на оригинал.

func (u User) Birthday() {
    u.Age++ // Это изменит только копию!
}

func main() {
    user := User{Name: "Боб", Age: 30}
    user.Birthday()
    fmt.Println(user.Age) // Все еще 30! Баг?
}

Такой код не скомпилируется с ошибкой, но логически он неверен. Вы ожидаете, что возраст увеличится, а он остается прежним. Добро пожаловать в мир value receiver!

Pointer receiver

Если же вы используете указатель: func (u *User) ..., метод получает ссылку на оригинальный объект. Теперь изменения будут видны всем.

func (u *User) Birthday() {
    u.Age++ // Изменяем оригинал
}

func main() {
    user := &User{Name: "Боб", Age: 30} // Создаем указатель
    user.Birthday()
    fmt.Println(user.Age) // 31! Работает!
}

Правило большого пальца: если метод изменяет состояние структуры или структура большая (чтобы избежать копирования), используйте pointer receiver. Если метод только читает данные — value receiver.

Как это работает под капотом?

Теперь самое интересное. Когда компилятор Go видит:

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

Он превращает это в обычную функцию, где receiver становится первым аргументом:

func Rectangle_Area(r Rectangle) float64 {
    return r.Width * r.Height
}

А вызов rect.Area() компилятор переписывает как Rectangle_Area(rect). Это и есть синтаксический сахар — удобная обертка над обычными функциями.

Для pointer receiver все аналогично:

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

Компилятор превращает в:

func Rectangle_Scale(r *Rectangle, factor float64) {
    r.Width *= factor
    r.Height *= factor
}

Вот и весь секрет! Никакой магии, только умные преобразования на этапе компиляции.

Реальный кейс: почему это важно?

Представьте, что вы пишете HTTP-сервер на Go. Типичный пример:

type Server struct {
    Port int
    DB   *sql.DB
}

func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) {
    // Тяжелая работа с БД
    s.DB.Query("SELECT ...")
    w.Write([]byte("OK"))
}

func main() {
    srv := &Server{Port: 8080, DB: initDB()}
    http.HandleFunc("/", srv.HandleRequest)
    http.ListenAndServe(":8080", nil)
}

Здесь srv.HandleRequest — это не просто вызов метода. Это method value. Go автоматически захватывает receiver и создает замыкание. Если бы вы использовали value receiver, то при каждом запросе создавалась бы копия сервера — со всеми вытекающими проблемами производительности и состояния.

Тонкие моменты: nil receiver и интерфейсы

В Go можно вызывать методы на nil указателе! Это не баг, а фича:

type MySlice []int

func (s *MySlice) Add(val int) {
    if s == nil {
        *s = &MySlice{val}
        return
    }
    *s = append(*s, val)
}

func main() {
    var s *MySlice = nil
    s.Add(1) // Работает! Метод сам инициализирует nil
    fmt.Println(s) // &[1]
}

Это часто используется для паттерна nil object или для безопасной инициализации.

Еще один важный момент: интерфейсы. Если тип реализует интерфейс с pointer receiver, то переменная этого типа (не указатель) не будет удовлетворять интерфейсу:

type Greeter interface {
    Greet() string
}

type User struct{ Name string }

// Метод с value receiver
func (u User) Greet() string { return u.Name }

// Оба варианта работают
var g1 Greeter = User{"Алиса"}
var g2 Greeter = &User{"Боб"}

// А теперь с pointer receiver                

Блоги

Рекомендаций по книгам