Что такое набор методов (Method Set) в Go и как компилятор находит методы?

Ответ

Разрешение (поиск) методов в Go происходит во время компиляции и строго определяется понятием набора методов (Method Set). У каждого типа в Go есть свой набор методов.

Правила определения набора методов

Существует два простых правила:

  1. Набор методов типа T включает все методы с ресивером (получателем) типа T.
  2. Набор методов типа *T (указатель) включает все методы с ресивером *T, а также все методы с ресивером T.

Проще говоря, указатель «знает» о методах своего типа значения, но не наоборот.

Как это работает на практике?

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

type User struct{
    Name string
}

// Метод с ресивером-значением (входит в Method Set User и *User)
func (u User) GetName() string {
    return u.Name
}

// Метод с ресивером-указателем (входит только в Method Set *User)
func (u *User) SetName(name string) {
    u.Name = name
}

func main() {
    // Переменная-значение
    u1 := User{Name: "Alice"}
    u1.GetName() // OK: GetName() есть в наборе методов User
    u1.SetName("Bob") // OK: Компилятор неявно преобразует u1 в &u1

    // Переменная-указатель
    u2 := &User{Name: "Charlie"}
    u2.GetName() // OK: Компилятор неявно преобразует u2 в *u2 (разыменовывает)
    u2.SetName("David") // OK: SetName() есть в наборе методов *User
}

Важные аспекты

  • Удовлетворение интерфейсов: Тип удовлетворяет интерфейсу, только если его набор методов содержит все методы этого интерфейса. Это ключевая причина, почему методы, изменяющие состояние (как SetName), обычно объявляются с ресивером-указателем. Если бы SetName был нужен для удовлетворения интерфейса, то только *User удовлетворял бы ему, а User — нет.

  • Встраивание (Embedding): При встраивании одного типа в другой, методы встроенного типа добавляются в набор методов внешнего типа, что позволяет вызывать их напрямую.

Ответ 18+ 🔞

А, слушай, вот этот ваш Go, он же, блядь, как хитрая жопа — с виду просто, а под капотом такие правила, что мама не горюй. Речь про то, как он методы ищет, когда ты их вызываешь. Так вот, запомни раз и нахуй: всё решается на этапе компиляции, по чёткому понятию «набора методов» (Method Set). У каждого типа свой такой набор, как личное досье.

Два железобетонных правила, хуле

Заруби себе на носу, чтобы не выебываться потом:

  1. У типа T (просто значение) в наборе — все методы, где ресивер (получатель) такой же — T.
  2. А вот у типа *T (указатель, звёздочка перед) — набор жирнее. Туда входят все методы с ресивером *T, плюс на халяву все методы с ресивером T.

Короче, если перевести на русский: указатель — он умный, он «знает» про методы своего значения. А вот просто значение — тупое, про методы указателя него нихуя не знает.

Как это выглядит в жизни, когда код пишешь?

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

type User struct{
    Name string
}

// Метод с ресивером-значением (в досье и у User, и у *User)
func (u User) GetName() string {
    return u.Name
}

// Метод с ресивером-указателем (в досье ТОЛЬКО у *User)
func (u *User) SetName(name string) {
    u.Name = name
}

func main() {
    // Просто переменная, не указатель
    u1 := User{Name: "Alice"}
    u1.GetName() // ОК: GetName() есть в досье у User
    u1.SetName("Bob") // И ЭТО ОК! Компилятор сам догадается сделать &u1

    // А вот переменная-указатель
    u2 := &User{Name: "Charlie"}
    u2.GetName() // Тоже ОК! Компилятор разыменует: *u2
    u2.SetName("David") // Ну тут всё ясно, SetName() в досье *User как родной
}

На что это всё влияет, кроме вызовов? А вот на что!

  • Интерфейсы, ёпта! Тип удовлетворяет интерфейсу только если его «досье» (набор методов) содержит все методы этого интерфейса. Вот почему методы, которые меняют состояние (типа SetName), почти всегда делают с ресивером-указателем. Представь: если бы интерфейс требовал SetName, то его бы удовлетворял только *User, а простой User — нихуя. Вот такой пиздец.

  • Встраивание (Embedding) — это вообще отдельный цирк. Когда один тип встраиваешь в другой, методы встроенного типа автоматом попадают в «досье» внешнего. Можно вызывать их напрямую, будто они свои. Удобно, блядь, но если не понимать, откуда ноги растут, можно и ебаклак получить.