Урок по треку Go 8 мин

Структуры, методы и интерфейсы

Ресиверы по значению и указателю, наборы методов, неявная реализация, nil-интерфейс, встраивание.

В Go нет классов и наследования. Есть структуры с методами, встраивание и интерфейсы, которые реализуются неявно.

Методы и ресиверы

type Counter struct{ n int }

func (c Counter) Value() int { return c.n } // ресивер по значению: работает с копией
func (c *Counter) Inc()      { c.n++ }      // по указателю: меняет оригинал

Указатель берите, если метод меняет состояние, если структура большая или если внутри есть то, что нельзя копировать (sync.Mutex). Правило хорошего тона: если хотя бы один метод на указателе, делайте на указателе все.

Go сам берёт адрес при вызове: c.Inc() для адресуемой переменной c превращается в (&c).Inc(). Но у значения из мапы или результата функции адреса нет, и такой вызов не скомпилируется.

Интерфейсы

Интерфейс это набор методов. Тип реализует интерфейс, если у него есть все методы, без слова implements.

type Stringer interface{ String() string }

Набор методов типа T включает только методы с ресивером T. Набор *T включает методы и на T, и на *T. Поэтому если String объявлен на *Counter, то var s Stringer = Counter{} не скомпилируется, а &Counter{} подойдёт.

Проверка на этапе компиляции: var _ Stringer = (*Counter)(nil).

Внутри интерфейса

Значение интерфейса это пара (тип, указатель на данные). Интерфейс равен nil, только если пусты обе части. Главная ловушка собеса:

func find() error {
    var e *MyErr = nil
    return e // интерфейс (тип *MyErr, nil) не равен nil!
}
if err := find(); err != nil { fmt.Println("сюрприз") }

Возвращайте просто nil, а не типизированный nil-указатель.

Приведение типов

s, ok := v.(fmt.Stringer) // ok == false, если не реализует
switch x := v.(type) {
case int:    fmt.Println("int", x)
case string: fmt.Println("string", x)
default:     fmt.Println("другое")
}

Без ok неудачное приведение вызывает panic.

Встраивание

type Logger struct{}
func (Logger) Log(s string) { fmt.Println(s) }

type Service struct {
    Logger          // встроенное поле
    name string
}
s := Service{}
s.Log("hi") // метод «поднялся» из Logger

Это композиция, а не наследование: внутри Log ресивер это Logger, он ничего не знает о Service. Если Service объявит свой Log, он затенит встроенный, а метод Logger вызывать придётся явно s.Logger.Log. Виртуальных методов нет: метод встроенного типа, вызывающий другой свой метод, всегда вызовет свою версию.

Интерфейсы тоже встраиваются: io.ReadWriter это Reader плюс Writer.

Сравнение структур

Структуры сравнимы ==, если сравнимы все поля. Интерфейсы сравниваются по динамическому типу и значению; если динамический тип несравним (слайс внутри), сравнение вызовет panic во время выполнения.

Советы по дизайну

  • Маленькие интерфейсы: один-два метода (io.Reader).
  • Интерфейс объявляет потребитель, а не реализация.
  • «Принимай интерфейсы, возвращай структуры».
  • any это псевдоним interface{}.

Что спросят на собесе

  • Когда ресивер по указателю, а когда по значению, что такое набор методов.
  • Почему интерфейс с nil-указателем внутри не равен nil.
  • Как устроено значение интерфейса (itab и data).
  • Чем встраивание отличается от наследования.
  • Где объявлять интерфейс и почему маленькие интерфейсы лучше.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (27)