Почему тип 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 шт):

Автор решения: Andrey Tabakov

В Go есть очень важное понятие таблица виртуальных функций - itable (иногда всплывает название vtable из-за C++, но сути не меняет).


Интерфейс в Go — это структура, которая содержит два поля:

  1. Указатель на конкретный объект (значение или указатель).
  2. Указатель на 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.

→ Ссылка
Автор решения: Pak Uula

В другом ответе используются 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 значение, у него нельзя вызывать методы с указателем в получателе.

→ Ссылка