---
title: "Интерфейсы"
description: "До сих пор мы работали с конкретными типами: знали точно, что перед нами int, string или наша собственная структура. Теперь представьте функцию, которая печатает отчёт о площади…"
canonical: https://graphlms.ru/go-basics/book/interfaces
course: "Основы Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Интерфейсы

> Источник: https://graphlms.ru/go-basics/book/interfaces — курс «Основы Go», GraphLMS.

## О чём глава

До сих пор мы работали с конкретными типами: знали точно, что перед нами `int`,
`string` или наша собственная структура. Теперь представьте функцию, которая
печатает отчёт о площади фигур. Фигуры бывают квадратные, круглые, треугольные,
и у каждой своя структура. Писать `describeSquare`, `describeCircle` и так
далее — тупик: на каждую новую фигуру новая функция. Функции на самом деле
неважно, что за фигура перед ней. Ей важно одно: фигура умеет посчитать свою
площадь.

Именно это и описывает **интерфейс** — набор методов, которые должен иметь тип.
Интерфейс говорит о поведении значения, а не о том, из каких полей оно состоит. В этой главе разберём, как
интерфейсы устроены в Go, почему реализация у них *неявная*, почему маленькие
интерфейсы лучше больших, что такое пустой интерфейс и `any`, и как безопасно
доставать конкретный тип обратно через type assertion и type switch.

## Интерфейс — это набор методов

Интерфейс перечисляет методы. Любой тип, у которого есть все эти методы,
*автоматически* удовлетворяет интерфейсу. Объявляется он так:

```go
type Stringer interface {
    String() string
}
```

Это значит: «`Stringer` — это что угодно, у чего есть метод `String() string`».
Никаких полей, никакой реализации — только список требований к поведению.

Переменная типа интерфейса может хранить значение любого подходящего типа.
Если у структуры `User` есть метод `String() string`, то так можно:

```go
var s Stringer = User{Name: "Аня"}
fmt.Println(s.String()) // вызовется метод String у User
```

Внутри значение интерфейса — это пара: *какой конкретный тип* там лежит и
*где лежат сами данные*. Поэтому через интерфейс можно вызывать методы, не зная
заранее, какой именно тип внутри. Эта пара ещё пригодится в главе об ошибках.

## Неявная реализация (duck typing)

В Go нет ключевого слова `implements`. Тип **не объявляет**, что реализует
интерфейс — он просто имеет нужные методы, и этого достаточно. Это и называют
«утиной типизацией»: если что-то ходит как утка и крякает как утка, для нас это
утка.

```go play
package main

import "fmt"

type Greeter interface {
	Greet() string
}

type Robot struct {
	ID int
}

// Просто метод с нужной сигнатурой — про Greeter нигде не сказано ни слова.
func (r Robot) Greet() string {
	return fmt.Sprintf("Бип-буп, я робот #%d", r.ID)
}

func main() {
	var g Greeter = Robot{ID: 7} // Robot подходит автоматически
	fmt.Println(g.Greet())
}
```

Вывод: `Бип-буп, я робот #7`.

Почему это удобно? Связь между типом и интерфейсом возникает *в точке
использования*, а не в точке объявления типа. Вы можете написать интерфейс уже
после того, как тип создан — хоть в чужом пакете — и тип «задним числом» начнёт
ему удовлетворять. Зависимости идут от потребителя к данным, а не наоборот.

## Функция, принимающая интерфейс

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

```go play
package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
}

type Square struct{ Side float64 }
type Circle struct{ R float64 }

func (s Square) Area() float64 { return s.Side * s.Side }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

// describe не знает и не хочет знать, что за фигура — лишь бы умела Area().
func describe(sh Shape) {
	fmt.Printf("Площадь = %.2f\n", sh.Area())
}

func main() {
	describe(Square{Side: 3})
	describe(Circle{R: 2})
}
```

Вывод:

```text
Площадь = 9.00
Площадь = 12.57
```

`describe` принимает `Shape`, а не `Square` или `Circle`.
Добавим завтра треугольник с методом `Area()` — `describe` примет его без единой
правки. Это и есть полиморфизм в Go: одно поведение, много типов.

## Маленькие интерфейсы — это хорошо

В Go хороший тон — делать интерфейсы *узкими*. Идеал — один-два метода.
Не зря самые знаменитые интерфейсы стандартной библиотеки крошечные:
`io.Reader` и `io.Writer` — по одному методу.

Почему меньше — лучше:

- **Меньше требований — больше типов подойдёт.** Интерфейс с одним методом
  удовлетворяет куча типов; с десятью — почти никто.
- **Проще тестировать.** Маленький интерфейс легко подменить заглушкой в тесте.
- **Чётче контракт.** Узкий интерфейс честно говорит, что именно нужно функции.

Отсюда важный принцип: **интерфейс объявляет тот, кто его потребляет**, а не тот,
кто реализует. Функции нужен «кто-то, кто умеет `Area()`» — пусть рядом с функцией
и живёт маленький `Shape`. Не нужно заранее придумывать гигантский интерфейс «на
все случаи».

```go
// Плохо: один разбухший интерфейс, который требует всё сразу.
type Animal interface {
    Eat()
    Sleep()
    Fly()
    Swim()
    Dig()
}

// Хорошо: маленькие интерфейсы под конкретные нужды.
type Flyer interface{ Fly() }
type Swimmer interface{ Swim() }
```

### Значение или указатель удовлетворяет интерфейсу?

Если метод объявлен с получателем `T`, им обладают и `T`, и `*T`. Если метод объявлен с получателем `*T`, интерфейсу с таким методом удовлетворяет только `*T`. Возможность написать `value.Method()` благодаря автоматическому взятию адреса не добавляет этот метод в набор методов самого `T`.

Например, если у `Counter` метод `func (c *Counter) Inc()`, а интерфейс объявлен как `type Incer interface{ Inc() }`, то переменной типа `Incer` можно присвоить `&Counter{}`, но нельзя `Counter{}`. Компилятор объяснит это прямо:

```text
cannot use Counter{} (value of struct type Counter) as Incer value in variable declaration:
Counter does not implement Incer (method Inc has pointer receiver)
```

Последние слова — подсказка: «метод `Inc` объявлен с получателем-указателем». Исправление — передавать адрес, `&Counter{}`. Вот почему в прошлой главе советовали не смешивать получатели: если у типа все методы на указателе, правило простое — в интерфейс кладём указатель.

## Пустой интерфейс и `any`

Интерфейс без методов — `interface{}` — не требует *ничего*. А раз требований нет,
ему удовлетворяет **любой** тип. Это «коробка, в которую можно положить что
угодно».

В современном Go для пустого интерфейса есть псевдоним `any` — читается приятнее,
а значит ровно то же самое:

```go
var x any         // то же, что interface{}
x = 42            // ок
x = "привет"      // ок
x = []int{1, 2}   // ок
```

`any` полезен там, где тип заранее неизвестен: например, `fmt.Println` принимает
`...any` и потому печатает что угодно. Но за гибкость приходится платить: пока
значение лежит в `any`, компилятор почти ничего о нём не знает — вы не можете
вызвать методы или сложить два таких значения. Чтобы снова получить конкретный
тип, его нужно *достать обратно*.

> Совет джуну: `any` — не «универсальный тип на каждый день». В обычном коде
> предпочитайте конкретные типы и узкие интерфейсы. `any` уместен, когда вы
> действительно работаете с заранее неизвестными данными.

## Type assertion: достаём конкретный тип

**Type assertion** вытаскивает из интерфейса значение конкретного типа.
Синтаксис — `x.(T)`:

```go
var x any = "текст"
s := x.(string)   // s == "текст"
```

Но что если внутри лежит *не* тот тип, который вы запросили? Форма с одним
значением в этом случае вызовет **панику**. Поэтому почти всегда используют
безопасную форму «comma-ok» — она возвращает второе булево значение `ok`:

```go play
package main

import "fmt"

func main() {
	var x any = 42

	if n, ok := x.(int); ok {
		fmt.Println("это int:", n*2)
	} else {
		fmt.Println("это не int")
	}

	// А здесь тип не совпадёт — паники нет, просто ok == false.
	if s, ok := x.(string); ok {
		fmt.Println("строка:", s)
	} else {
		fmt.Println("внутри не строка, а что-то ещё")
	}
}
```

Запомните правило: **форма с одним значением паникует при несовпадении, форма с
`ok` — нет**. В рабочем коде по умолчанию берите comma-ok, а голую форму — только
когда вы абсолютно уверены в типе и хотите упасть, если ошиблись.

## Type switch: разбор по типам

Когда вариантов несколько, городить лесенку из `if ... ok` неудобно. Для этого есть
**type switch** — `switch` по динамическому типу значения:

```go play
package main

import "fmt"

func describe(v any) string {
	switch val := v.(type) {
	case int:
		return fmt.Sprintf("целое число %d", val)
	case string:
		return fmt.Sprintf("строка длиной %d", len(val))
	case bool:
		return fmt.Sprintf("булево %t", val)
	default:
		// сюда попадёт всё, для чего нет своей case-ветки —
		// например float64, ведь case float64 мы не написали.
		return "неизвестный тип"
	}
}

func main() {
	fmt.Println(describe(7))
	fmt.Println(describe("Go"))
	fmt.Println(describe(true))
	fmt.Println(describe(3.14)) // float64 — ветки нет, ловит default
}
```

Конструкция `v.(type)` работает только внутри `switch`. В каждой `case`-ветке
переменная `val` уже имеет соответствующий конкретный тип, так что с ней можно
работать как обычно — никаких дополнительных assertion не нужно.

## `fmt.Stringer`: реальный интерфейс из stdlib

Тот самый `Stringer` из начала главы на самом деле есть в стандартной
библиотеке, в пакете `fmt`. Это, пожалуй, первый интерфейс, который вы
реализуете сами:

```go
type Stringer interface {
    String() string
}
```

Функции пакета `fmt` (`Println`, `Printf` с `%v` и `%s`, `Sprint`) проверяют:
а нет ли у значения метода `String() string`? Если есть — печатают именно его
результат вместо стандартного представления. То есть вы задаёте, как ваш тип
*выглядит в выводе*.

```go play
package main

import "fmt"

type Temperature float64

// Реализуем fmt.Stringer — теперь fmt знает, как нас печатать.
func (t Temperature) String() string {
	return fmt.Sprintf("%.1f°C", float64(t))
}

func main() {
	t := Temperature(21.5)
	fmt.Println(t)             // вызовется наш String()
	fmt.Printf("сейчас %v\n", t)
}
```

Вывод:

```text
21.5°C
сейчас 21.5°C
```

Мы нигде не сказали `fmt`, что наш тип — `Stringer`: пакет сам проверил, есть ли
у значения нужный метод. Это и есть неявная реализация в действии.

## Частые ошибки

**Голая type assertion и паника.** `x.(int)` без `ok` уронит программу, если внутри
не `int`. По умолчанию пишите `n, ok := x.(int)` и проверяйте `ok`.

**`any` вместо нормального типа.** Соблазнительно сделать параметр `any` и «не
думать о типах». Но тогда компилятор перестаёт вас защищать, а вызывающему коду
приходится гадать. Конкретный тип или узкий интерфейс почти всегда лучше.

**Слишком большой интерфейс.** Интерфейс с кучей методов трудно реализовать и
подменить в тесте. Дробите на маленькие — по одному-двум методам под конкретную
нужду.

**Забыли, что метод должен совпасть точно.** Тип удовлетворяет интерфейсу, только
если сигнатуры методов совпадают *буква в букву*: имя, аргументы, возвращаемые
значения. Лишний параметр или другое имя — и реализации «не существует», а ошибка
вылезет лишь при попытке присваивания.

## Попробуйте сами

В примере с `Shape` добавьте `Triangle` с полями `Base` и `Height` типа `float64`. Реализуйте площадь как половину произведения основания на высоту. Передайте треугольник `Base: 4, Height: 3` в существующую `describe`, не меняя эту функцию. Почему компилятор принимает новый тип?

### Проверка

Вывод — `Площадь = 6.00`:

```go
type Triangle struct {
    Base   float64
    Height float64
}

func (t Triangle) Area() float64 {
    return t.Base * t.Height / 2
}
```

В `main` добавьте `describe(Triangle{Base: 4, Height: 3})`. Метод `Area() float64` совпадает с требованием `Shape`; отдельное объявление реализации не нужно. Если изменить результат метода на `int`, тип перестанет удовлетворять этому интерфейсу.

## Что дальше

Самый распространённый интерфейс Go вы уже видели много раз, не зная об этом.
Это `error` — интерфейс с единственным методом `Error() string`. В следующей главе,
[«Ошибки как значения»](/go-basics/book/errors), вы будете создавать свои ошибки,
оборачивать их и доставать из них конкретный тип — тем же приёмом, что и type
assertion здесь. А когда понадобится обобщённость без потери типов, которую не
даёт `any`, помогут [дженерики](/go-basics/book/generics).
