Ответ
Разрешение (поиск) методов в Go происходит во время компиляции и строго определяется понятием набора методов (Method Set). У каждого типа в Go есть свой набор методов.
Правила определения набора методов
Существует два простых правила:
- Набор методов типа
T
включает все методы с ресивером (получателем) типаT
. - Набор методов типа
*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): При встраивании одного типа в другой, методы встроенного типа добавляются в набор методов внешнего типа, что позволяет вызывать их напрямую.