Интерфейсы: основы, 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-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.