Раздел 22 · Go

Интерфейсы: основы, any, type assertion, embedding, nil

junior~70 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Перед этим уроком

Интерфейсы: основы, any, type assertion, embedding, nil

Интерфейсы в Go устроены проще, чем в любом другом популярном языке. Нет implements, нет иерархий, нет сложных дженериковых ограничений. Тип реализует интерфейс просто потому, что у него есть нужные методы. Это называется structural typing, и это самая мощная идея языка.

Этот урок длинный, потому что в Go-коде интерфейсы повсюду: io.Reader, io.Writer, error, fmt.Stringer, http.Handler, sort.Interface. Поняв, как они устроены, ты дешифруешь любой Go-код.

1. Что такое интерфейс

Интерфейс это контракт, набор методов:

type Greeter interface {
    Greet() string
}

Любой тип, у которого есть метод Greet() string, автоматически удовлетворяет этому интерфейсу. Ничего объявлять не надо:

type Russian struct{}
func (r Russian) Greet() string { return "Привет" }

type Spanish struct{}
func (s Spanish) Greet() string { return "Hola" }

func sayHi(g Greeter) {
    fmt.Println(g.Greet())
}

sayHi(Russian{}) // "Привет"
sayHi(Spanish{}) // "Hola"

Это неявная реализация. Сравни с Java/C#:

class Russian implements Greeter { ... }

В Go отсутствие implements это плюс: интерфейс можно объявить после того, как уже написал типы. И один тип может удовлетворять множеству интерфейсов, не зная об их существовании.

2. Каноничные интерфейсы stdlib

Большинство Go-кода построено вокруг четырёх-пяти простых интерфейсов. Запомни их сразу.

error

type error interface {
    Error() string
}

Один метод. Любой тип с Error() string это error. Подробно в 11-errors-deep.

fmt.Stringer

type Stringer interface {
    String() string
}

fmt-пакет смотрит на этот интерфейс и автоматически использует String() для печати. Поэтому fmt.Println(myEnum) “просто работает”, если у твоего enum есть String().

io.Reader и io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Два самых важных интерфейса для I/O. Их реализуют: *os.File, *bytes.Buffer, *strings.Reader, net.Conn, http.ResponseWriter, *gzip.Writer. Если функция принимает io.Reader, ты можешь подложить что угодно реализующее Read. Это dependency injection без слова “DI”.

func WordCount(r io.Reader) int {
    scanner := bufio.NewScanner(r)
    scanner.Split(bufio.ScanWords)
    count := 0
    for scanner.Scan() {
        count++
    }
    return count
}

WordCount(os.Stdin)
WordCount(strings.NewReader("a b c"))
WordCount(file)

Этот же WordCount работает с stdin, со строкой, с файлом, с HTTP-телом. Ноль изменений в функции.

io.Closer и сборные интерфейсы

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

type ReadWriter interface {
    Reader
    Writer
}

Это встраивание интерфейсов. ReadCloser означает “оба метода”, тип должен реализовать и Read, и Close. Используется в HTTP-теле (req.Body, например), в файлах, и где угодно где есть открытие и закрытие.

3. Как использовать интерфейсы

Интерфейс это тип в Go, как int или *User. Можно:

  • объявить переменную типа интерфейс,
  • передавать как параметр функции,
  • возвращать из функции,
  • класть в slice или map.
var w io.Writer
w = os.Stdout         // *os.File реализует io.Writer
w.Write([]byte("hi")) // печатает в stdout

w = &bytes.Buffer{}   // и Buffer реализует
w.Write([]byte("hi")) // в буфер

Шаблон, чаще всего встречающийся в коде:

type UserRepo interface {
    Get(id int) (User, error)
    Save(u User) error
}

type service struct {
    users UserRepo
}

func (s *service) UpdateName(id int, name string) error {
    u, err := s.users.Get(id)
    if err != nil { return err }
    u.Name = name
    return s.users.Save(u)
}

Сервис не знает, идёт ли репозиторий в Postgres, в память, или в mock для теста. Это самая полезная функция интерфейсов.

Правило: accept interfaces, return structs

accept interfaces, return structs:

// хорошо
func WordCount(r io.Reader) int            // принимаем абстракцию
func NewServer(...) *Server                // возвращаем конкретное

// плохо
func WordCount(r *os.File) int             // привязали к файлу
func NewServer(...) ServerInterface        // лишний интерфейс на возврате

Возвращать конкретный тип позволяет вызывающему пользоваться всеми методами, не только теми, что в интерфейсе. Принимать интерфейс делает функцию переиспользуемой.

4. interface и any

Особый интерфейс, пустой:

type any = interface{}

any это псевдоним interface{} со времён Go 1.18, означающий “любой тип”. Любая переменная любого типа удовлетворяет any, потому что требований к ней нет.

func describe(v any) {
    fmt.Printf("%T: %v\n", v, v)
}

describe(42)         // int: 42
describe("hello")    // string: hello
describe([]int{1,2}) // []int: [1 2]

Когда any оправдан:

  • fmt.Println(...any), печать чего угодно;
  • JSON Unmarshal в неизвестную структуру, var data any;
  • map[string]any, “произвольный JSON-объект”.

Когда any это запах:

  • Бизнес-логика, “получаем заказ как any”. Это ослабление типов. Лучше определить интерфейс или конкретный тип.
  • Generic-замена через any, до 1.18 так делали Stack, теперь бери generics из 12-generics-basics.

5. Type assertion: проверить и достать

Раз any “стирает тип”, надо как-то достать его обратно. Это type assertion:

var i any = "hello"

s := i.(string)     // s == "hello", тип string
fmt.Println(s)

n := i.(int)         // panic: interface conversion: any is string, not int

Безопасная форма с , ok:

n, ok := i.(int)    // n == 0, ok == false (не паника)
if ok {
    fmt.Println(n)
}

Используй safe-форму везде, кроме случаев когда точно знаешь тип.

Type switch: ассерчинг сразу нескольких

Когда вариантов несколько:

func describe(v any) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int %d", x)
    case string:
        return fmt.Sprintf("string %q", x)
    case []int:
        return fmt.Sprintf("[]int len=%d", len(x))
    case error:
        return fmt.Sprintf("error %v", x)
    default:
        return fmt.Sprintf("unknown %T", x)
    }
}

switch v.(type) это специальный синтаксис, читается “переключение по типу”. Внутри каждого case x уже имеет нужный тип.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.