Интерфейс в Go (Golang) — это тип, который описывает набор методов, то есть поведение, а не данные. Любой тип, у которого есть все эти методы, реализует интерфейс неявно: слова implements в Go нет, соответствие проверяет компилятор. Переменная интерфейсного типа хранит пару «конкретный тип + значение» и позволяет вызывать методы, не зная, что лежит внутри.
Содержание
- Мини-словарь
- Минимальный пример: объявление и неявная реализация
- io.Writer: интерфейс из стандартной библиотеки
- Встраивание интерфейсов
- Пустой интерфейс interface{} и any
- Type assertion и type switch
- Интерфейсы-ограничения в дженериках
- Частые ошибки: неверно -> результат -> исправление
- Как проектировать интерфейсы
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — объявление и реализация, пустой интерфейс interface{} и его синоним any, извлечение конкретного типа через type assertion и type switch, io.Writer как главный пример из стандартной библиотеки и три типичные ошибки. Все примеры проверены на Go 1.27.1 (linux/arm64), вывод взят из реального запуска.
Мини-словарь
- Интерфейсный тип — объявление
type X interface { ... }, список методов. - Конкретный тип — struct, int, срез и другие типы, у которых есть реальные данные и методы.
- Интерфейсное значение — то, что лежит в переменной типа
X: динамический тип и значение этого типа. Если внутри ничего нет, значение равноnil. - Набор методов (method set) — методы, доступные у типа. У
Tи у*Tон разный, от этого зависит, кто реализует интерфейс.
Минимальный пример: объявление и неявная реализация
Аналогия: «пишущий инструмент» — не конкретный предмет, а умение писать. Карандаш и ручка им являются, потому что пишут. В Go так же: talker — это «все, что умеет talk()».
package main
import "fmt"
// talker - интерфейс: любой тип с методом talk() string
type talker interface {
talk() string
}
type martian struct{}
func (m martian) talk() string {
return "nack nack"
}
type laser int
func (l laser) talk() string {
return fmt.Sprintf("pew x%d", int(l))
}
// shout работает с любым talker и не знает о конкретных типах
func shout(t talker) {
fmt.Println(t.talk() + "!")
}
func main() {
var t talker // нулевое значение интерфейса - nil
t = martian{}
shout(t)
t = laser(3)
shout(t)
fmt.Printf("%T %v\n", t, t)
}
Программа напечатает:
nack nack!
pew x3!
main.laser 3
Что здесь важно:
martian(struct без полей) иlaser(целое число) нигде не упоминаютtalker. Они реализуют его просто потому, что у них есть методtalk() stringс точно такой сигнатурой.- Функция
shoutпринимает интерфейс. Если завтра появится новый тип сtalk(),shoutзаработает с ним без изменений — в этом и смысл интерфейсов. %Tпоказывает динамический тип внутри интерфейсной переменной: сейчас тамmain.laser.
Имена интерфейсов с одним методом по соглашению строят суффиксом «-er»: talker, io.Writer, io.Reader, fmt.Stringer. Это традиция стандартной библиотеки, а не требование компилятора.
Если метода нет, программа не соберется. Для типа stone struct{} без talk() строка var t talker = stone{} дает ошибку компиляции:
./main.go:10:17: cannot use stone{} (value of struct type stone) as talker value in variable declaration: stone does not implement talker (missing method talk)
Чтобы проверка шла сразу при сборке, даже если тип нигде не присваивается интерфейсу, используют строку var _ io.Writer = (*countingWriter)(nil) — она есть в примере с io.Writer ниже.
io.Writer: интерфейс из стандартной библиотеки
io.Writer объявлен в пакете io с единственным методом Write(p []byte) (n int, err error). Его реализуют os.Stdout и открытые файлы (*os.File), *bytes.Buffer, *strings.Builder, HTTP-ответ в обработчике (http.ResponseWriter), сжимающий *gzip.Writer. Поэтому одна функция может писать куда угодно:
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
)
// report пишет в любой io.Writer: терминал, буфер, файл, HTTP-ответ
func report(w io.Writer, name string, score int) error {
_, err := fmt.Fprintf(w, "%s: %d\n", name, score)
return err
}
// countingWriter оборачивает другой Writer и считает байты
type countingWriter struct {
w io.Writer
n int
}
func (c *countingWriter) Write(p []byte) (int, error) {
n, err := c.w.Write(p)
c.n += n
return n, err
}
func main() {
report(os.Stdout, "stdout", 1)
var buf bytes.Buffer
report(&buf, "buffer", 2)
fmt.Print("из буфера -> ", buf.String())
var sb strings.Builder
cw := &countingWriter{w: &sb}
report(cw, "builder", 3)
report(cw, "Привет", 4)
fmt.Printf("%q, байт: %d\n", sb.String(), cw.n)
// compile-time проверка: *countingWriter реализует io.Writer
var _ io.Writer = (*countingWriter)(nil)
}
Вывод:
stdout: 1
из буфера -> buffer: 2
"builder: 3\nПривет: 4\n", байт: 27
countingWriter сам является io.Writer и оборачивает другой io.Writer — так устроены многие обертки в стандартной библиотеке. Счетчик показывает байты, а не символы: «Привет» в UTF-8 занимает 12 байт, поэтому итог 27, а не 21.
В учебном примере ошибка report в main не проверяется ради краткости. В рабочем коде результат записи в файл или сеть проверяют всегда: Write может вернуть ошибку или записать меньше байт, чем передано.
Встраивание интерфейсов
Интерфейс можно собрать из других интерфейсов. Так в пакете io объявлен io.ReadWriter: все методы Reader плюс все методы Writer.
package main
import (
"bytes"
"fmt"
"io"
)
// встраивание: readWriter = все методы io.Reader + все методы io.Writer
type readWriter interface {
io.Reader
io.Writer
}
func main() {
var rw readWriter = &bytes.Buffer{}
fmt.Fprint(rw, "hello")
data, _ := io.ReadAll(rw)
fmt.Println(string(data))
var w io.Writer = rw // шире -> уже: проверка не нужна
_, isReader := w.(io.Reader)
fmt.Println(isReader)
}
Программа выводит hello и true. Присвоить «широкий» интерфейс «узкому» можно без проверок. Обратное направление — только через type assertion (о ней ниже): в w статически виден лишь Write, но динамический тип *bytes.Buffer умеет и Read, поэтому проверка возвращает true.
Пустой интерфейс interface{} и any
У interface{} нет ни одного метода, поэтому его реализует любой тип. С Go 1.18 есть встроенный синоним any — это ровно тот же тип, не новый. Именно так объявлены параметры fmt.Println(a ...any).
any полезен там, где тип действительно заранее неизвестен: форматированный вывод, разбор произвольного JSON в map[string]any, контейнеры до появления дженериков. Цена — компилятор больше не проверяет тип, и достать значение обратно можно только проверкой во время выполнения. Если функция работает с набором известных типов, чаще подходят обычный интерфейс с методами или дженерики.
Type assertion и type switch
Type assertion v.(T) достает из интерфейсного значения конкретный тип T (или проверяет, что значение реализует другой интерфейс). Type switch делает то же для нескольких вариантов сразу.
package main
import "fmt"
func describe(v any) string {
switch x := v.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int, x*2 = %d", x*2)
case string:
return fmt.Sprintf("string длиной %d байт", len(x))
case fmt.Stringer:
return "Stringer: " + x.String()
case error:
return "error: " + x.Error()
default:
return fmt.Sprintf("другой тип %T", x)
}
}
type point struct{ x, y int }
func (p point) String() string { return fmt.Sprintf("(%d;%d)", p.x, p.y) }
func main() {
values := []any{42, "Привет", point{1, 2}, fmt.Errorf("нет файла"), 3.14, nil}
for _, v := range values {
fmt.Println(describe(v))
}
var v any = "Go"
s, ok := v.(string)
fmt.Println(s, ok)
n, ok := v.(int)
fmt.Println(n, ok)
_ = v.(int) // без ok: паника
}
Вывод (последние строки трассировки сокращены):
int, x*2 = 84
string длиной 12 байт
Stringer: (1;2)
error: нет файла
другой тип float64
nil
Go true
0 false
panic: interface conversion: interface {} is string, not int
Разбор:
- Внутри каждой ветки
caseпеременнаяxуже имеет тип этой ветки: вcase intс ней можно считать, вcase stringбратьlen. - Ветки проверяются по порядку.
pointпопал вfmt.Stringer, потому что у него естьString(). Если значение подходит под несколько интерфейсов, сработает первая подходящая ветка. - Форма
s, ok := v.(T)безопасна: при несовпаденииok == false, аs— нулевое значениеT. Форма безokпри несовпадении вызывает панику, поэтому ее берут, только когда тип гарантирован логикой программы. - В тексте паники
anyпоказан какinterface {}— это одно и то же.
| Задача | Что использовать |
|---|---|
| Вызвать поведение, не зная типа | метод интерфейса |
| Проверить один тип или интерфейс | x, ok := v.(T) |
| Разобрать несколько вариантов | switch x := v.(type) |
Найти ошибку нужного типа в цепочке %w |
errors.AsType[*E](err) (Go 1.26+) или errors.As, а не assertion |
| Одна функция для набора числовых типов | дженерики с ограничением |
Интерфейсы-ограничения в дженериках
С Go 1.18 интерфейс может содержать не только методы, но и набор типов: ~int | ~float64. Такой интерфейс — ограничение (constraint) для параметров дженериков, а не обычный тип переменной.
package main
import "fmt"
type Number interface {
~int | ~float64
}
func Sum[T Number](xs []T) T {
var s T
for _, x := range xs {
s += x
}
return s
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{0.5, 0.25}))
}
Вывод: 6 и 0.75. Если объявить переменную var n Number = 5, компилятор откажет:
./main.go:10:8: cannot use type Number outside a type constraint: interface contains type constraints
Частые ошибки: неверно -> результат -> исправление
1. Метод с приемником-указателем
Неверно: метод объявлен у *robot, а в интерфейс кладут значение robot.
package main
import "fmt"
type talker interface {
talk() string
}
type robot struct{ name string }
// метод с приемником-указателем
func (r *robot) talk() string {
return "I am " + r.name
}
func main() {
var t talker = robot{name: "R2"}
fmt.Println(t.talk())
}
Результат — ошибка компиляции:
./main.go:17:17: cannot use robot{…} (value of struct type robot) as talker value in variable declaration: robot does not implement talker (method talk has pointer receiver)
Причина: в набор методов типа robot входят только методы с приемником-значением, а talk объявлен у *robot. Исправление — класть указатель:
package main
import "fmt"
type talker interface {
talk() string
}
type robot struct{ name string }
func (r *robot) talk() string {
return "I am " + r.name
}
func main() {
var t talker = &robot{name: "R2"} // исправлено: указатель
fmt.Println(t.talk())
r := robot{name: "C3"}
fmt.Println(r.talk()) // прямой вызов у адресуемой переменной работает: Go берет &r сам
}
Вывод: I am R2 и I am C3. Прямой вызов r.talk() работает, потому что Go сам берет адрес переменной. Для присваивания интерфейсу такого автоматического & нет.
2. Интерфейс с nil-указателем внутри не равен nil
Неверно: функция возвращает error, внутри которого лежит типизированный nil-указатель.
package main
import (
"errors"
"fmt"
)
type myErr struct{}
func (*myErr) Error() string { return "my error" }
// неверно: возвращает типизированный nil-указатель через интерфейс error
func check(fail bool) error {
var e *myErr // nil-указатель
if fail {
e = &myErr{}
}
return e
}
// исправлено: при отсутствии ошибки возвращаем литерал nil
func checkFixed(fail bool) error {
if fail {
return &myErr{}
}
return nil
}
func main() {
err := check(false)
fmt.Println(err == nil) // false
fmt.Printf("%T %v\n", err, err)
fmt.Println(checkFixed(false) == nil) // true
var target *myErr
fmt.Println(errors.As(check(true), &target))
}
Вывод:
false
*main.myErr my error
true
true
Интерфейсное значение равно nil, только когда пусты и тип, и значение. В check(false) тип есть (*main.myErr), значение — nil-указатель, поэтому err != nil, и вызывающий код решит, что ошибка произошла. Исправление — возвращать литерал nil, а не переменную конкретного типа. Строка my error печатается потому, что Error() не обращается к полям; метод, который разыменует nil-указатель, вызвал бы панику. go vet в Go 1.27.1 этот код не отметил, так что полагаться на него здесь нельзя.
3. Type assertion без ok
Неверно: n := v.(int), когда в v может быть строка. Результат — panic: interface conversion: interface {} is string, not int (показано выше). Исправление — форма n, ok := v.(int) с проверкой ok или type switch с веткой default.
Еще одна ловушка того же рода: сравнение == двух any, внутри которых несравнимые типы (срезы, map, функции), компилируется, но падает при выполнении с panic: runtime error: comparing uncomparable type []int.
Как проектировать интерфейсы
Это соглашения сообщества Go (руководство Go Code Review Comments, стандартная библиотека), а не правила компилятора:
- Маленькие интерфейсы. Один-два метода, как
io.Reader/io.Writer. Большой интерфейс трудно реализовать в тестах и в альтернативных реализациях. - Интерфейс объявляет потребитель. Пакет, которому нужно «что-то с методом
Save», сам описывает этот интерфейс; пакет-поставщик обычно возвращает конкретный тип. Отсюда поговорка «принимай интерфейсы, возвращай структуры» — с исключениями вродеerror. - Не заводить интерфейс заранее. Если реализация одна и подмена в тестах не нужна, интерфейс добавляет лишний уровень косвенности. Его вводят, когда появляется вторая реализация или фейк для теста.
Выводы
- Интерфейс в Go описывает поведение набором методов; тип реализует его неявно, если у него есть все методы с точными сигнатурами.
- Интерфейсное значение — пара «динамический тип + значение»; оно равно
nil, только когда пусты обе части, отсюда ловушка с nil-указателем вerror. any— синонимinterface{}с Go 1.18; достать конкретный тип обратно можно черезv.(T)сokили type switch.io.Writerпоказывает главную пользу интерфейсов: одна функция пишет в терминал, буфер, файл или сеть без изменений.- Интерфейсы с наборами типов (
~int | ~float64) — только ограничения для дженериков, переменную такого типа объявить нельзя.
Где применяется / связь с практикой
Освойте тему на практике
Интерфейсы в Go встречаются в каждом рабочем сервисе: обработчики HTTP (http.Handler), слой хранения за интерфейсом репозитория, фейки в модульных тестах, middleware-обертки над io.Writer и http.ResponseWriter. Систематически пройти язык от типов и методов до интерфейсов, горутин и тестирования можно на курсе «Go-разработчик. Базовый уровень». Разборы отдельных тем с преподавателями бывают на открытых уроках Otus.
FAQ
Может ли один тип реализовать несколько интерфейсов?
Да. *bytes.Buffer одновременно io.Reader, io.Writer, fmt.Stringer и другие интерфейсы — достаточно иметь нужные методы.
Медленнее ли вызов через интерфейс, чем прямой?
Вызов через интерфейс косвенный и обычно мешает встраиванию функции, поэтому в горячих циклах разница бывает измеримой (сборка с PGO, начиная с Go 1.21, умеет девиртуализировать часть горячих вызовов). В обычном коде она обычно не заметна; решать стоит по результатам бенчмарка go test -bench, а не заранее.
Можно ли использовать интерфейсное значение как ключ map?
Можно, если динамический тип внутри сравнимый. Если там окажется срез или map, код скомпилируется, но запись по такому ключу упадет во время выполнения: panic: runtime error: hash of unhashable type []int.



