Почему тип Human НЕ реализует интерфейс Runner
Уважаемые коллеги, пожалуйста помогите с каверзным вопросом по go)
Известно , что реализация метода интерфейса Runner в типе Human принимает указатель на тип:
`func (h *Human) Run() string`
Почему тип Human НЕ реализует интерфейс Runner ? Почему указатель на тип Human реализует тип Runner ?
type Runner interface {
Run() string
}
type Human struct {
Name string
}
func (h *Human) Run() string {
return fmt.Sprintf("Человек %s бегает", h.Name)
}
func main() {
var runner Runner
john := Human{"John"}
runner = john // ERROR
runner = &john // так правильно
}
Почему ошибка ? ведь функция run() соответствует сигнатуре в интерфейсе Runner Ведь если метод принимает указатель на тип то мы можем вызвать этот метод и у самого типа и у указателя и ошибки не будет:
john.Run() //нет ошибки
(&john).Run()//нет ошибки
Почему тогда в случае интерфейса компилятор ругается, что в тип Runner можно присвоить только указатель, компилятор говорит это происходит так как Реализация метода интерфейса имеет тип-приемник указатель ???
func (h *Human) Run() string
================= А вот если наоборот сделать и в реализации функции интерфейса в качестве типа-приемника мы передадим НЕ указатель на тип , а сам тип, то переменной типа Runner можно присваивать И сам ТИП И указатель на ТИП
func (h Human) Run() string
runner = john так правильно
runner = &john и так правильно
Ответы (2 шт):
В Go есть очень важное понятие таблица виртуальных функций - itable (иногда всплывает название vtable из-за C++, но сути не меняет).
Интерфейс в Go — это структура, которая содержит два поля:
- Указатель на конкретный объект (значение или указатель).
- Указатель на itable - таблицу методов, которая связывает методы интерфейса с конкретными методами типа.
Itable строится на этапе компиляции и содержит соответствие между методами интерфейса и методами конкретного типа. Именно эта таблица определяет, реализует ли тип интерфейс.
Есть два основных вида присвоения методов: Pointer receiver и Value receiver
1. Метод с pointer receiver:
func (h *Human) Run() string { ... }
- Метод привязан к указателю на тип
*Human. - В таблице методов (itable) для типа
*Human(указатель на структуру) есть запись дляRun(). - В таблице методов для типа
Human(конкретной структуры) нет методаRun().
2. Метод с value receiver:
func (h Human) Run() string { ... }
- Метод привязывается к значению, у которого тип
Human. - В таблице методов для
HumanестьRun(). - Самое интересное: Для
*Humanтоже можно вызватьRun(), так как компилятор автоматически разыменует указатель(*h).Run().
Для каждого типа (Human, *Human) и интерфейса (Runner) компилятор строит отдельную itable.
Когда вы присваиваете значение интерфейсу, Go проверяет, содержит ли его itable типа все методы интерфейса.
Когда вы пытаетесь присвоить значение Human переменной типа Runner:
func (h *Human) Run() string { ... }
...
var runner Runner
john := Human{"John"}
runner = john // Ошибка!
- Компилятор проверяет, есть ли в itable для типа
HumanметодRun(). Его нет, так как метод привязан к указателю (pointer receiver)*Human. - Интерфейс
Runnerтребует, чтобы тип имел методRun(), ноHumanего не предоставляет.
А вот с &john это сработает нормально, так как у указателя этот метод есть:
runner = &john // OK
- Тип
*Humanимеет методRun()в своей itable. - Интерфейс
Runnerвидит, что*Humanреализует его требования.
Самое непонятное на мой взгляд это неявное приведение к указателю:
john.Run() // Преобразуется в (&john).Run()
Компилятор Go автоматически подставляет взятие адреса, если метод требует pointer receiver. Почему так происходит описано в соседнем ответе. Но это работает только для вызовов методов, а не для присвоения переменной типа интерфейса. Интерфейсы требуют точного соответствия типов в itable.
В другом ответе используются itables. Это объяснение кочует со страницы на страницу, несмотря на то, что устарело уже лет десять тому как. itable были в первых компиляторах Go, написанных ещё на Си. Уже давненько компилятор Go написан на Go, и в нём используется method set. И в спецификации языка Go, кстати, тоже: https://go.dev/ref/spec#Method_sets
Объект x типа Typ можно присвоить переменной интерфейсного типа I только в том случае, если множество методов интерфейса вложено в множество методов типа Typ.
Как формируются множества методов для типов.
Пусть у нас есть неуказательный тип T, и тип указателей на T - *T.
Если метод определен как func (T) F(..) { ... }, то этот метод входит в множество методов:
- типа T, так как он указан в сигнатуре получателя
(T), - типа
*T, так как в множество методов указателей входят все методы базового типа.
Если метод определен как func (*T) G(..) { ... }, то этот метод входит в множество методов:
- типа
*T, так как он указан в сигнатуре получателя(*T), - и всё... тип
Tнет, так как базовый тип не реализует методы указателя.
Вот и вашем случае, метод Run объявлен для типа *Human, и, следовательно, не определен для типа Human. Поэтому тип Human не реализует интерфейс Runner - у него в множестве методов нет метода Run.
Неформально method set разбирают в го-вики: https://go.dev/wiki/MethodSets
Теперь о переменной.
Если значение имеет тип T, значение адресуемо (addressable), и метод G есть в наборе методов типа *T, то x.G() есть синтаксический сахар для (&x).G()
Здесь очень важно, что значение должно быть addressable. Когда вы пишете john := Human{}, то компилятор аллоцирует память, и выражение john становится адресуемым. И компилятор спокойно примет от вас john.Run(), интерпретируя это как (&john).Run(). Однако этот синтаксический сахар не меняет тип переменной john на *Human.
Кстати, если вы напишете Human{}.Run(), то компилятор вас поругает. Human{} - это временное, non-addressable значение, у него нельзя вызывать методы с указателем в получателе.