Почему если В golang композиция типов это НЕ НАСЛЕДОВАНИЕ, то структуру-"наследника" можно поместить на то место, где ожидали ВСТРОЕННЫЙ в нее тип

  • Почему в golang композиция типов НЕ является НАСЛЕДОВАНИЕМ ?
  • Почему Тип B , в который ВСТРОИЛИ в тип А теперь можно подставлять в места, где ожидался тип А ?
  • Перефразирую: пусть есть интерфейс ААА и тип А его реализующий. Тип А встроили в тип В с помощью композиции и теперь везде , где ожидался тип/интерфейс ААА можно подставить тип В и компилятор не выдает ошибку , из этого я делаю вывод , что тип В полноценно наследует тип А с его полями и методами (и реализует ААА)
  • Почему это неверный вывод?

Рассмотрим пример:

type WrapperWriter struct {
    http.ResponseWriter
    StatusCode int
} 
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        next.ServeHTTP(w, r)
    })
}
  • Метод next.ServeHTTP(w, r) принимает первым параметром любую структуру, которая реализует интерфейс ResponseWriter
  • Если мы теперь первым параметром вместо ResponseWriter подставим тип WrapperWriter то ошибка не возникнет
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        wrapper := &WrapperWriter{
            ResponseWriter: w,
            StatusCode:     200,
        }

        next.ServeHTTP(wrapper, r)
    })
}
  • Я понимаю это так - ошибки нет так как WrapperWriterэто композиция ResponseWriter и на типе WrapperWriter можно вызывать методы типа ResponseWriter и НАВЕРНОЕ поэтому компилятор думает что WrapperWriter реализует ResponseWriter. Но это не так ведь у методов в ПРИЕМНИКЕ типа стоит по прежнему ResponseWriter и методы вызываются на самом деле у ResponseWriter , а не у WrapperWriter даже если визуально кажется наоборот, из этого следует что WrapperWriter НЕ ДОЛЖЕН реализовывать интерфейс ResponseWriter.
  • Тогда почему везде где ожидается ResponseWriter можно вставить WrapperWriter и ничего не сломается

Но все это псевдонаследование работает только если в типе

type WrapperWriter struct {
    http.ResponseWriter
    StatusCode int
} 

поле http.ResponseWriter встропено анонимно то есть без имени

  • Почему если встроить поле с именем , то тип WrapperWriter уже нельзя подставлять на место где ожидается тип встроенного поля ResponseWriter ? и методы ResponseWriter уже нельзя напрямую вызвать на типе WrapperWriter
type WrapperWriter struct {
    ResponseWriter http.ResponseWriter
    StatusCode int
} 

Ответы (2 шт):

Автор решения: Alexander Pavlov

Это называется promoted embedded field

https://go.dev/ref/spec#Struct_types

A field declared with a type but no explicit field name is called an embedded field. An embedded field must be specified as a type name T or as a pointer to a non-interface type name *T, and T itself may not be a pointer type or type parameter. The unqualified type name acts as the field name.

...

A field or method f of an embedded field in a struct x is called promoted if x.f is a legal selector that denotes that field or method f.

...

If S contains an embedded field T, the method sets of S and *S both include promoted methods with receiver T. The method set of *S also includes promoted methods with receiver *T.

А когда ты добавляешь имя, то поле перестаёт быть embedded (ну и, соответственно, перестаёт быть promoted.

Фактически, это переключатель между агрегированием и наследованием.

type WrapperWriter struct {
    http.ResponseWriter
    StatusCode int
}

в Java будет

class WrapperWriter extends http.ResponseWriter {
 int StatusCode;
}

а вот это

type WrapperWriter struct {
    Writer http.ResponseWriter
    StatusCode int
}

в Java будет

class WrapperWriter {
 http.ResponseWriter Writer; 
 int StatusCode;
}
→ Ссылка
Автор решения: Pak Uula

Видов наследований есть множество, но главное, что объединяет их всех - отношение instance-of. Объект типа-потомка является одновременно объектом типа предка.

Возьмём пример в Java:

    public static class A {
        public String fieldA;
        public A(String fieldA) {
            this.fieldA = fieldA;
        }
        public String getFieldA() {
            return fieldA;
        }
    }

    public static class B extends A {
        public B(String s) {
            super(s);
        }
    }
    public static void main(String[] args) {
        A a = new A("Hello");
        B b = new B("World");
        System.out.println(a.getFieldA()); // Output: Hello
        System.out.println(b.getFieldA()); // Output: World

        A aa = b;
   }

Компилируется без ошибок.

В Go отношение instance-of по отношению к "предку" нарушено.


type A struct {
    FieldA string
}

func (a A) MethodA() string {
    return a.FieldA
}

type B struct {
    A
}

func main() {
    b := B{
        A: A{FieldA: "Hello"},
    }

    var a A = b
}

На присваивании a = b компилятор Го выругается, в отличие от компилятора Java в аналогичной ситуации.

Но при этом все интерфейсы, которые реализуют типы A и *A, реализуют и типы B и *B. Это называется duck typing - ходит как утка, крякает как утка - значит, утка. "Наследование интерфейсов" достигается за счёт того, что в Go релизация интерфейсов не декларативная, как в Java, а по факту:

  • Java: для того, чтобы тип A реализовал интерфейс I, необходимо это декларировать class A implements I.
  • Go: для того, чтобы тип А реализовал интерфейс I, небходимо, чтобы в method set типа A наличествовали все методы из интерфейса I.

А теперь следите за руками. Если поле включено в структуру без имени, то методы типа этого поля включаются в method set самой структуры (а методы с получателем-указателем включаются в method set указателя на структуру). Следовательно, если тип A реализовал интерфейс I, то тип struct{ A } тоже реализует этот интерфейс, ведь методы A входят в набор методов "наследника", и тем самым "наследник" реализует интерфейс.

Это работает даже в тех случаях, когда встраивают интерфейс.

package main

import "fmt"

type A interface {
    MethodA() string
}

type B struct {
    A
}

type AImpl struct {
    FieldA string
}

func (a *AImpl) MethodA() string {
    return a.FieldA
}

func main() {
    b := B{
        A: &AImpl{FieldA: "Hello"},
    }

    fmt.Println(b.MethodA())
}

Единственное, что нужно соблюдать при встраивании интефейсов - инциализация. Вы должны подставить на место встроенного поля объект, тип которого реализует интерфейс. В этом примере поле B.A является интерфейсом, поэтому нужно подставить какой-то конкретный объект - &AImpl{}, так как интерфейс A реализует тип *AImpl.

По этой схеме реализован тип WrapperWriter. Благодаря встраиванию типа http.ResponseWriter сам WrapperWriter реализует этот интерфейс.

Но сама по себе реализация странная. Так как метод Write([]byte) (int, error) на самом деле реализуется полем WrapperWriter.ResponseWriter, то поле StatusCode этот метод игнорирует. Бессмыслица какая-то.

→ Ссылка