В 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).
- Чем встраивание отличается от наследования.
- Где объявлять интерфейс и почему маленькие интерфейсы лучше.