---
title: "Полиморфизм и пустой интерфейс"
description: "Полиморфизм в Go: срезы интерфейсов, пустой интерфейс any (бывший interface{}), fmt.Println(a ...any) изнутри, цена any — type safety и ловушка typed-nil."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/11-polimorfizm/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Полиморфизм и пустой интерфейс

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 11 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/11-polimorfizm/*

полиморфизм go — запрос, который вводят, когда интерфейсы уже освоены и хочется понять, как далеко заходит идея «один контракт — много типов». Прошлый урок обещал тройную развязку: заглянуть внутрь пары «динамический тип + значение», познакомиться с any — пустым интерфейсом, которому удовлетворяет всё, — и честно разобрать цену этой гибкости. Исполняем: срезы интерфейсов и срезы any, fmt.Println(a ...any) изнутри, type safety как аргумент против any-зависимости, и обещанное углубление ловушки typed-nil — функция, которая возвращает (*MyErr)(nil) как error и обманывает проверку вызывающего. Все примеры запускаются прямо на странице.

### Полиморфизм в Go: один контракт — много типов

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

В [десятом уроке](/courses/go-advanced/lessons/10-interfejsy/) мы уже видели этот механизм в работе: срез `[]fmt.Stringer` хранил кота, пса и робота, и каждый отвечал на `String()` своим текстом. Сегодня — та же фигура, но глубже: разберём, почему она работает, а затем пойдём в крайность. Крайность называется **пустой интерфейс**: контракт без единого метода, которому удовлетворяет буквально всякое значение в программе. У него есть официальное короткое имя — `any` — и тяжёлая репутация: это самый гибкий и самый опасный тип в языке. Мы разберём и его срезы (`[]any`), и его роль внутри `fmt.Println`, и то, почему компилятор перестаёт вас защищать, как только вы пишете `any` вместо конкретного контракта.

План урока: углубить срез интерфейсов из прошлого урока, вскрыть any и его историю (`interface{}` до версии 1.18, алиас `any` после), запустить свой вариадик `...any`, сравнить any с конкретным интерфейсом на живом примере, углубить обещанную ловушку typed-nil до производственной формы `func f() error`, и обозначить границы — где any уместен, а где дженерики (тринадцатый урок). Интерактивы спроектированы под оба движка песочницы: там, где движки расходятся, будет честная оговорка.

### Срез интерфейсов глубже: что происходит внутри цикла

Начнём с повторения, но с подсветкой механики. Объявление `zoo := []Speaker{Cat{...}, Dog{...}, Robot{...}}` делает три вещи. Во-первых, создаёт срез, каждый элемент которого — интерфейсное значение `Speaker`: пара «динамический тип + значение», а не структура и не указатель. Во-вторых, при каждом присваивании элемента компилятор сверяет набор методов конкретного типа с контрактом: у `Cat` есть `Speak()` — вписывается, у `int` нет — не впишется, ошибка сборки. В-третьих, цикл `for _, s := range zoo` на каждом шаге зовёт `s.Speak()` — и вызов уходит динамическому типу текущего элемента. Вот весь полиморфизм: **одна точка вызова, много реализаций, решение принимается по данным, а не по коду**.

Ключевое следствие пары «тип + значение»: добавление нового типа не требует трогать старый код. Написали `Bird` с методом `Speak()` — и он вписывается в существующий срез, потому что контракт проверяется по фактам набора методов, а не по родословной. Именно поэтому в прошлом уроке мы называли это расширением без правок: срез, цикл и интерфейс остаются нетронутыми, растёт только множество типов под контрактом.

И ещё одна деталь, которая сегодня станет главной героиней: если бы контракта не было вовсе — если бы срез принимал *всё* — компилятор перестал бы проверять хоть что-нибудь. Цикл не знал бы, какой метод звать: у него не было бы ни одного гарантированного. Эта пустота — не абстракция, а конкретный тип: `any`. Разберёмся, откуда он взялся и как устроен.

### Пустой интерфейс any: interface{} до 1.18, алиас после

Интерфейс — это набор методов. Логичный предельный случай: набор из **нуля методов**. Такой контракт не требует ничего — и потому ему удовлетворяет каждое значение в программе: число, строка, структура, функция, карта, другой интерфейс. Это и есть **пустой интерфейс**. Исторически он записывался как `interface{}` — слово interface, пустые фигурные скобки: «набор методов, перечисленный внутри, — пуст». Так писали с первых версий Go, и в миллионах строк существующего кода вы встретите именно эту форму.

В Go 1.18 (релиз с дженериками, до которого мы доберёмся в тринадцатом уроке) пустому интерфейсу дали короткий алиас: **`any`**. Алиас — это второе имя того же типа, а не новый тип: `var v any` и `var v interface{}` — буквально одно и то же, компилятор разворачивает одно в другое. В свежем коде принято писать `any`; встречая `interface{}`, читайте «any». В этом уроке мы пишем `any`, но все примеры работают и со старой формой — проверить можно в любом из ранимых блоков.

Внутри any — всё та же пара «динамический тип + значение». Пустая пара, когда any-переменная объявлена без присваивания, — это `nil`; заполненная пара после `v = 42` — (`int`, 42); после `v = "семь"` — (`string`, "семь"). Любое присваивание any-переменной — это упаковка: значение вместе со своим типом заворачивается в пару. Отсюда два следствия, которые мы сегодня проверим руками. Первое: any-параметр функции принимает всё — сигнатура `func show(v any)` — универсальный зев. Второе, неприятное: у any **нет ни одного метода**, значит функция с any-параметром не может вызвать у аргумента ничего — только передать дальше или распаковать. Компиляторные гарантии, ради которых мы брали конкретные контракты, исчезли. Это и есть цена, за которую платят гибкостью.

### any-параметр: одна функция — любые типы

Запустите: одна и та же функция приняла int, string, float64 и bool — каждое значение упаковалось в пару «тип + значение» и напечаталось fmt-ом. В этом весь any-параметр: сигнатура func show(v any) не задаёт ни одного требования, поэтому подходит каждому типу. Эксперимент: попробуйте передать два аргумента сразу — show(1, 2) — и программа не соберётся: any-параметр ровно один, вариадиком он становится только с многоточием ...any (ниже в этом уроке). И обратите внимание: внутри show нельзя вызвать ни один метод аргумента — кроме печати fmt-ом с ним делать нечего.

```go
package main

import "fmt"

// show — функция с any-параметром: принимает значение ЛЮБОГО типа
func show(v any) {
        fmt.Println("получено:", v)
}

func main() {
        show(42)
        show("привет")
        show(3.14)
        show(true)
}
```

### fmt.Println(a ...any) изнутри: вы уже пользовались any

Заглянем в исходники стандартной библиотеки, которые вы зовёте каждый день. Пакет fmt объявляет печать так:

`func Println(a ...any) (n int, err error)`

Вот откуда берётся магия «печатай что угодно»: **fmt.Println принимает вариадик any** — срез пустых интерфейсов, в который упаковываются все ваши аргументы. Передали `fmt.Println("баланс:", 1000, "ок", true)` — fmt получил `[]any` из четырёх элементов, прошёл по срезу циклом, для каждого элемента посмотрел на динамический тип и выбрал формат. Динамический тип решает всё: для string — сырой текст, для int — цифры, а если динамический тип реализует `fmt.Stringer` — зовётся метод `String()` (десятый урок). Значит, когда вы пишете `fmt.Println(кот)` и получаете красивую строку — это полиморфизм в чистом виде: одна функция печати, сотни типов, решение по данным.

Тот же приём у `fmt.Printf(format string, a ...any)`, `errors.New` в паре с `fmt.Errorf(format string, a ...any)` и у половины пакета fmt. Вариадик any — идиома «принимаю что угодно, разберусь сам»: она законна там, где функция *специализируется на незнании* — печать, сериализация, логирование. Но у неё есть жёсткая граница: внутри такой функции с каждым элементом можно сделать только то, что умеет any, — то есть почти ничего, кроме передачи дальше. Хочется вызвать метод — нужен конкретный контракт, а не any.

### printAll: свой вариадик ...any

Запустите: printAll собрал разнородные аргументы в срез []any и прошёл циклом — каждый элемент напечатан отдельно. Внутри цикла vals — это обычный срез any: len, range, индексация работают как у любого среза. Последняя строка — сама fmt.Println с четырьмя аргументами: упаковка в []any происходит и там, и там, разница только в том, что fmt заодно вставляет пробелы и перевод строки. Эксперимент: допишите вызов printAll(true, -7, "хвост") — функция даже не заметит, что аргументы изменились: вариадик any принимает всё.

```go
package main

import "fmt"

// printAll — своя версия Println: ...any — любое число значений ЛЮБЫХ типов
func printAll(vals ...any) {
        for _, v := range vals {
                fmt.Println(v)
        }
}

func main() {
        printAll(1, "два", 3.5)
        fmt.Println("fmt.Println внутри устроен так же:")
        fmt.Println("частей:", 3, 3.5, true)
}
```

### []any: срез разных типов — сила и цена

Раз any влезает в параметр, он влезает и в срез: **`[]any`** — срез, где в одной корзине лежат значения разных типов, каждое упакованное в свою пару «тип + значение». Это самый гибкий контейнер в Go: `box := []any{42, "кот", 3.14, true}` собирается без вопросов, `append` доливает элементы любого типа, `len` и `range` работают как обычно. Внутри `box` нет ни одного значения «в чистом виде» — все упакованы, и компилятор не помнит ни одного конкретного типа.

Сравните с `[]fmt.Stringer` из прошлого урока. Внешне фигуры похожи: и там, и там срез интерфейсных пар, и там, и там полиморфизм. Разница — в контракте. `[]fmt.Stringer` гарантирует: у каждого элемента есть метод `String() string`, и цикл может **звать его легально** — компилятор проверил при упаковке. `[]any` не гарантирует ничего: у элементов нет ни одного общего метода, цикл может их только печатать fmt-ом или перекладывать дальше. Формула простая: **чем пустее контракт, тем больше типов влезает — и тем меньше можно с ними сделать**. `[]fmt.Stringer` — корзина «всё, что умеет печатать себя»; `[]any` — корзина «всё».

Где `[]any` всё-таки нужен? Там, где разнородность — суть данных: распакованный JSON с неизвестной заранее структурой (так устроен `json.Unmarshal` в `any`), печать отладочных значений, слои кода, которые перекладывают данные, не вникая, что внутри. Правило вкуса: если для ваших данных существует осмысленный контракт хотя бы из одного метода — берите контракт, а не any; any оставьте на случаи, когда контракта действительно нет.

### Что выведет коробка any?

```go
package main

import "fmt"

func main() {
        box := []any{7, "семь", 3.5, true}
        fmt.Println("размер:", len(box))
        for _, v := range box {
                fmt.Println(v)
        }
        box = append(box, "пятый")
        fmt.Println("стало:", len(box))
        fmt.Println(box[4])
        fmt.Println(box[0], box[3])
}
```

**Что выведет программа?**

- Программа не соберётся: в []any нельзя смешивать int, string и bool
- ✅ размер: 4, затем 7 / семь / 3.5 / true, затем стало: 5, пятый и 7 true
- Паника при печати 3.5: типы в срезе не совпадают
- размер: 1 — срез схлопнется к общему типу первого элемента

[]any — легальный срез интерфейсных пар: смешивать типы в нём можно без ограничений, потому что каждый элемент упакован в пару «динамический тип + значение». len считает 4, цикл печатает каждый элемент по его динамическому типу, append доливает строку — стало 5. Последняя строка печатает два элемента разом: 7 (int) и true (bool) — fmt упаковывает их в свой внутренний []any и печатает через пробел. Смешивание типов запрещено в срезах КОНКРЕТНЫХ типов ([]int не примет строку), но не в []any.

### Type safety: зачем any — последний выбор

Теперь честный разговор о цене. **Type safety** — типобезопасность — это свойство языка, при котором классы ошибок ловятся компилятором до запуска: вызов несуществующего метода, сложение строки с числом, передача аргумента не того типа. Go в обычной жизни типобезопасен жёстко, и это его главный подарок рефакторингу: переименовали поле — компилятор указал на все места использования. Проблема any в том, что **он выключает эту защиту локально**: как только значение упаковано в пару «any + значение», компилятор забывает динамический тип и перестаёт проверять всё, что вы с ним делаете.

По запросу **any interface golang** поисковая выдача быстро приводит к дискуссиям «any considered harmful» и к правилам линтеров, запрещающим any в публичных API. Аргумент всегда один и тот же, и мы его уже видели в коде: у any нет методов. `var v any = c` принимает кота — но `v.String()` не скомпилируется; `box := []any{...}` соберётся — но `box[0] + 1` не скомпилируется; функция `func show(v any)` съест всё — но не сможет вызвать у аргумента ничего. Каждый шаг «сделаем тип свободнее» — это шаг «сделаем компилятор слепее». Ошибки не исчезают: они переезжают из стадии компиляции в стадию исполнения, где за них платит пользователь паниками и мусорными данными.

Отсюда рабочий критерий. any — это **признание незнания**: «на этом слое кода я не знаю и не хочу знать, что внутри, — я перекладываю, печатаю или сериализую». Если вы знаете, что аргумент умеет печатать себя, — берите `fmt.Stringer`; если умеет читать, — `io.Reader`; если это число или строка — берите конкретный тип. Пустой интерфейс — крайний случай, когда честного контракта нет. Уместен он в fmt-пакете, в распаковке JSON, в редких служебных слоях — и почти никогда в бизнес-логике. А ещё полезно помнить направление эволюции языка: дженерики в 1.18 появились во многом затем, чтобы закрыть главную причину, по которой люди тянулись к any, — «нужна функция для многих типов». Об этом — тринадцатый урок.

### any против fmt.Stringer: что теряется

Запустите: обе переменные приняли одного кота — и fmt.Stringer, и any хранят пару (Cat, Барсик), поэтому v == nil даёт false: тип в паре заполнен. Печать s даёт «кот Барсик», потому что fmt видит Stringer и зовёт String(). А вот с v контракта нет: раскомментируйте в редакторе строку fmt.Println(v.String()) — и программа не соберётся: у any нет метода String, компилятор честно откажется. Эксперимент: замените тип переменной v на fmt.Stringer и убедитесь, что та же строка собирается — контракт вернул компилятору зрение.

```go
package main

import "fmt"

// Cat реализует fmt.Stringer — метод String() string
type Cat struct{ name string }

func (c Cat) String() string { return "кот " + c.name }

func main() {
        c := Cat{name: "Барсик"}

        // конкретный контракт: знает метод String()
        var s fmt.Stringer = c

        // пустой контракт: не знает ни одного метода
        var v any = c

        // fmt.Println(v.String()) // раскомментируйте: у any нет метода String
        fmt.Println("оба приняли одно значение")
        fmt.Println("v == nil:", v == nil)
        fmt.Println("s:", s)
        v = 42
        fmt.Println("v переприсвоен:", v)
}
```

### Проверьте себя

**Вопрос:** box := []any{42, "кот", true}. Что из этого НЕЛЬЗЯ сделать с элементом box[0] без дополнительных инструментов?

- Напечатать его через fmt.Println(box[0])
- Посчитать len(box)
- ✅ Прибавить к нему единицу: box[0] + 1
- Передать его в другую функцию с any-параметром

У any нет методов и нет операций конкретных типов: компилятор видит пару «динамический тип + значение», но забыл, какой именно тип, поэтому арифметика box[0] + 1 не скомпилируется. Печать fmt-ом законна (fmt сам разбирается с динамическим типом), len(box) считает элементы среза, а передача в any-параметр — просто перекладывание той же пары. Чтобы вернуть элементу «настоящую» типизацию, нужна распаковка — type assertion, это территория двенадцатого урока.

### Typed-nil глубже: func f() error возвращает (*MyErr)(nil)

Десятый урок обещал вернуться к ловушке typed-nil «в одиннадцатом и двенадцатом уроках» — время платить. Напомним базу: интерфейс — пара «динамический тип + значение»; nil-интерфейс — пара из двух nil; присваивание типизированного nil-указателя заполняет половину пары, и `== nil` для интерфейса становится false. Тогда мы смотрели на это на переменных; в проде ловушка живёт в **функциях, возвращающих error** — а error, как мы помним, интерфейс из одного метода `Error() string`.

Каноническая производственная ошибка выглядит так: функция-помощник объявляет переменную-ошибку заранее и возвращает её в конце:

`func f() error { var e *MyErr = nil; ...; return e }`

Похоже на безобидный стиль «объявим наверху, вернём внизу». Но `return e` возвращает не «ничего», а пару `(*MyErr, nil)`: динамический тип в паре **заполнен**. Вызывающий делает `if err != nil` — и получает true: ошибка «есть», хотя внутри nil. Дальше обычно следует паника при попытке вызвать `err.Error()` или, хуже, молчаливый отправленный клиенту фиктивный "500". И обратите внимание: форма `return (*MyErr)(nil)` — литеральный типизированный nil — даёт тот же эффект: **err != nil — TRUE**.

Способ избежать — один и абсолютный: **возвращайте голый nil явно**. `return nil` кладёт пару (nil, nil), проверка вызывающего работает. Практическое правило: в ветке «ошибки нет» пишите `return nil` дословно, а не `return e` с e, объявленной где-то наверху; если стилистика «один return» нужна, возвращайте конкретный тип-указатель, а не интерфейс, либо собирайте результат через именованные возвраты аккуратно. Проверять это удобнее всего компилятором-соседом: `var _ error = (*MyErr)(nil)` гарантирует контракт, а привычка `return nil` — честность пары.

### Ловушка и починка рядом

```go
package main

import "fmt"

// MyErr — ошибка-структура с указательным методом Error() string
type MyErr struct{ msg string }

func (e *MyErr) Error() string { return e.msg }

// ЛОВУШКА: в ветке успеха возвращается (*MyErr, nil) — err != nil, хоть внутри nil
func badFetch(ok bool) error {
        var e *MyErr = nil // объявили заранее
        if !ok {
                e = &MyErr{msg: "не найдено"}
        }
        return e // всегда возвращает заполненный динамический тип!
}

// ПОЧИНКА: ветка успеха возвращает голый nil — пара (nil, nil)
func goodFetch(ok bool) error {
        if !ok {
                return &MyErr{msg: "не найдено"}
        }
        return nil // вызывающий увидит err == nil — честно
}

var _ error = (*MyErr)(nil) // compile-проверка контракта из десятого урока

func main() {
        // настоящий Go: badFetch(true) != nil — true, обман проверки
        fmt.Println(badFetch(true) != nil)
        // настоящий Go: goodFetch(true) == nil — true, проверка честная
        fmt.Println(goodFetch(true) == nil)
}
```

badFetch объявляет e *MyErr заранее и возвращает её всегда: даже с nil внутри пара получает динамический тип *MyErr, поэтому проверка err != nil у вызывающего срабатывает — классическая typed-nil ловушка. goodFetch возвращает голый return nil в ветке успеха — пара (nil, nil), проверка честная. Формы &MyErr{...} и var e *MyErr = nil показаны в статичном блоке: резервный движок песочницы адресные литералы и (*T)(nil) не исполняет, а главный движок, кроме того, сам сворачивает typed-nil к nil на пути error — в настоящем Go (локальный go run) main напечатает true и true: badFetch не равна nil, goodFetch равна.

### Проверьте себя ещё раз

**Вопрос:** func f() error { var e *MyErr = nil; return e }. Что вернёт проверка err == nil у вызывающего в НАСТОЯЩЕМ Go?

- true: e равна nil, значит и err равна nil
- ✅ false: return e упаковывает пару (*MyErr, nil) — динамический тип в паре заполнен
- false, но только если у MyErr есть другие методы
- Программа не соберётся: *MyErr нельзя вернуть как error

Это производственная форма ловушки typed-nil. error — интерфейс, return e возвращает пару «динамический тип + значение»: (*MyErr, nil). Тип заполнен — err == nil даёт false, проверка вызывающего обманута, хотя внутри nil. Лекарство — return nil явно: голый nil кладёт пару (nil, nil). Нюанс песочницы: ОБА движка на этом сюжете отвечают неверно (говорят true) — главный движок сворачивает typed-nil к nil на пути error, резервный делает то же самое; настоящий Go и локальный go run говорят false, как в этом вопросе.

> **Честно о двух движках песочницы.** Интерактивы этого урока (оба any-раннера, срезы []any, printAll, predict-ы, fill и order) дают одинаковый результат в обоих движках — основном (настоящий Go, yaegi в WASM) и резервном упрощённом. Расхождений три, и все честные. Первое: печать any, внутри которого лежит СТРУКТУРА с методом String: основной движок печатает фигурные скобки с полями ({Барсик}), резервный зовёт String и печатает «кот Барсик», настоящий Go зовёт String — поэтому в ранимых блоках any-структуры не печатаются вовсе, а значения под fmt.Stringer печатаются через типизированную переменную, где движки совпадают. Второе: type switch резервный движок не поддерживает — при попытке он отвечает «type switch (x.(type)) не поддерживается в песочнице; используйте type assertion x.(T)», поэтому type switch живёт в статичном блоке. Третье, самое хитрое: на ловушке typed-nil через error оба движка ошибаются одинаково — отвечают true (то есть «err == nil»), тогда как настоящий Go говорит false; поэтому ловушка разобрана в тексте и quiz-е, а не в ранимом коде.

### Type switch: упреждающий тизер двенадцатого урока

Мы дважды упёрлись в одну стену: any-значение не даёт вызвать ни метод, ни операцию. Как из пары «тип + значение» достать тип обратно? У Go есть специальная конструкция — **type switch**, переключение по динамическому типу:

`switch x := v.(type) { case int: ... case string: ... }`

На каждой ветке переменная x — уже конкретного типа: в ветке `case int` это int, и арифметика снова легальна; в `case string` — строка. Это «распаковка пары» на максимуме: конструкция перебирает типы и выбирает совпавший с динамическим. Родственный ей инструмент — одиночный **type assertion** `v.(T)` с ok-формой `x, ok := v.(T)`: «достань T, и скажи честно, получилось ли». Обе конструкции — это то, что превращает any из чёрной дыры в управляемый контейнер, и это целиком территория **двенадцатого урока**: там будут и правила паники одиночной формы, и идиомы ok-формы, и ошибки interface conversion.

Зачем мы вообще это упоминаем? Затем, что границу между «хорошим» и «плохим» any вы теперь можете формулировать точно: any на границе системы (печать, сериализация) с распаковкой type assertion-ом внутри — рабочий паттерн; any, который течёт по бизнес-логике без распаковки, — долг. Двенадцатый урок даст инструменты распаковки, тринадцатый — дженерики, которые убирают саму потребность в any для «функции для многих типов». Ниже — статичный пример type switch, чтобы конструкция осела в глазах; запустить его в резервном движке нельзя, основной компилирует честно.

### Type switch взглядом на двенадцатый урок

```go
package main

import "fmt"

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

func main() {
        fmt.Println(describe(42))
        fmt.Println(describe("хвост"))
        fmt.Println(describe(3.14))
}
```

switch x := v.(type) перебирает типы в case-ах и выбирает совпадающий с динамическим типом пары: int ловит 42, string — «хвост», 3.14 проваливается в default. Внутри ветки x — конкретный тип, поэтому %d и конкатенация легальны. Резервный движок песочницы конструкцию не парсит (ответит «type switch (x.(type)) не поддерживается в песочнице»), основной движок и настоящий Go исполняют этот код ровно с этим выводом. Полный разбор type assertion и ok-формы — в двенадцатом уроке.

### Жизненный цикл any-переменной

```go
package main

import "fmt"

func main() {
        var v any
        fmt.Println("до:", v == nil)
        v = 0
        fmt.Println("ноль:", v == nil)
        v = nil
        fmt.Println("снова:", v == nil)
}
```

**Что выведет программа?**

- false, false, false: any всегда что-то содержит
- ✅ true, false, true: нулевая пара пуста, присваивание нуля её заполняет, явный nil возвращает
- true, true, true: any не различает ноль и nil
- Программа не соберётся: any нельзя присвоить nil

var v any — пустая пара (nil, nil): сравнение с nil даёт true. Присваивание v = 0 упаковывает пару (int, 0): тип заполнен, поэтому v == nil уже false — ноль это ЗНАЧЕНИЕ, а не отсутствие значения. Присваивание v = nil возвращает пару в состояние (nil, nil) — снова true. Это та же механика, что у nil-интерфейсов десятого урока, просто контракт пуст: any — тоже пара, и правила сравнения с nil у всех интерфейсов общие.

### Впишите any-программу

Восстановите типы: параметр-всё-принимающий, срез пустых интерфейсов и проверку размера — по одному фрагменту на пропуск.

```go
package main

import "fmt"

func describe(v ___) {
        fmt.Println(v)
}

func main() {
        box := ___any{42, "текст", true}
        for _, v := range box {
                describe(v)
        }
        fmt.Println(len(box) == ___)
}
```

Подсказка: Первый пропуск — имя пустого интерфейса; второй — пустые скобки среза перед any; третий — количество элементов в срезе.

Правильные ответы в пропусках: `any`, `[]`, `3`

### Соберите срез-коробку

Соберите программу: срез []any с тремя разнотипными значениями и цикл, который печатает каждый элемент.

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `func main() {`
4. `box := []any{"голос", 7, true}`
5. `for _, v := range box {`
6. `fmt.Println(v)`
7. `}`
8. `}`

### Шпаргалка: any и его семья

| Инструмент | Что это | Правило | Осторожно |
| --- | --- | --- | --- |
| any | пустой интерфейс: контракт из нуля методов, ему удовлетворяет всё | алиас interface{} с Go 1.18; старая форма — то же самое | у any нет методов: вызывать у значения нечего, компилятор слеп |
| []any | срез пар «динамический тип + значение» разных типов | len, range, append работают; смешивать типы легально | арифметика и методы над элементами не скомпилируются |
| func show(v any) | универсальный параметр: принимает любой тип | законно для печати/сериализации/перекладывания | внутри функции с аргументом нельзя сделать ничего осмысленного |
| fmt.Println(a ...any) | печать через вариадик any | fmt разбирает динамический тип; Stringer зовёт String() сам | многоточие ...any обязательно: один any — один аргумент |
| type switch | switch v.(type): распаковка динамического типа по веткам | в каждой case переменная конкретного типа | резервный движок не поддерживает; детали — в двенадцатом уроке |
| typed-nil: return e | var e *MyErr = nil; return e как error | возвращайте голый return nil в ветке успеха | пара (*MyErr, nil): err != nil — TRUE в настоящем Go; оба движка песочницы врут |

### Правила гигиены any

- any — признание незнания: берите его, только когда контракта не существует вовсе (печать, сериализация, слои перекладывания), а не «на всякий случай».
- Если для значений существует контракт хотя бы из одного метода — берите контракт ([]fmt.Stringer вместо []any): type safety вернётся компилятором в первый же рефакторинг.
- Помните алиас: any с Go 1.18 — это interface{}; в старом коде формы равнозначны, в новом пишите any.
- Возвращая error, пишите return nil дословно в ветке успеха: return e с заранее объявленной *MyErr упаковывает пару (*MyErr, nil) и обманывает проверку err != nil — ловушка typed-nil.
- Распаковку any делайте type switch-ом или type assertion-ом у границы системы; any без распаковки, текущий по бизнес-логике, — технический долг.
- Для «одной функции с разными типами» сначала подумайте о дженериках (тринадцатый урок) — они дают гибкость без потери type safety.

### Что дальше: распаковка и дженерики

Пройден ещё один виток: [срезы интерфейсов](/courses/go-advanced/lessons/10-interfejsy/) получили внутреннюю механику — пара «динамический тип + значение» объясняет и полиморфный цикл, и то, почему typed-nil обманывает проверку; пустой интерфейс `any` показал себя и универсальным зевом (`func show(v any)`, `fmt.Println(a ...any)`), и дырой в type safety. Вы умеете выбирать между `[]fmt.Stringer` и `[]any` осознанно: первый — корзина поведения, второй — корзина незнания. Контекст из [девятого урока](/courses/go-advanced/lessons/09-context/) и каналы четвёртого из середины курса теперь стоят в одном ряду с интерфейсами: это всё инструменты контрактов — только контракты там про отмену и передачу данных, а здесь про поведение типов.

Поиск **any interface golang** теперь ведёт вас дальше по той же тропе. Следующие два шага закрывают оставленные хвосты. [«Приведение типов и type assertion»](/courses/go-advanced/lessons/12-type-assertion/) — двенадцатый урок: одиночная форма `v.(T)` с её паникой interface conversion, ok-форма `x, ok := v.(T)`, type switch во всей красе — инструменты честной распаковки any, про которые сегодня только тизер. Тринадцатый урок — дженерики: `func Map[T, R any](...)...` и то, как язык 1.18 научился давать гибкость «для многих типов», не выключая type safety, — прямая альтернатива many-случаям, где раньше писали any. [Весь курс](/courses/go-advanced/) движется к проектированию API — с типизированными инструментами вместо пустых корзин.

### Карточки урока

- **Полиморфизм в Go — как достигается** — Значения разных конкретных типов под одним контрактом отвечают на один вызов по-разному: переменная интерфейсного типа хранит пару «динамический тип + значение», вызов уходит динамическому типу. Без наследования: новый тип с нужным методом сам вписывается в существующие срезы и циклы.
- **any и interface{} — что это и как связаны** — Пустой интерфейс: контракт из нуля методов, которому удовлетворяет любое значение. До Go 1.18 писали interface{}; с 1.18 any — алиас того же типа (не новый тип). Внутри — та же пара «динамический тип + значение»; пустая пара — nil.
- **[]any — что хранит и чем отличается от []fmt.Stringer** — Срез пар «тип + значение» разных типов: len/range/append работают, смешивать типы легально. []fmt.Stringer гарантирует метод String() и позволяет звать его; []any не гарантирует ничего — элементы нельзя ни распечатать методом, ни сложить: у any нет методов. Корзина поведения против корзины незнания.
- **Type safety — что ломает any** — Компилятор забывает динамический тип: у any нет ни одного метода, арифметика и сравнения конкретных типов не компилируются. Ошибки переезжают из стадии сборки в рантайм. Правило: any — на границе (печать, JSON), контракт или конкретный тип — в логике; распаковка — type assertion/switch (урок 12), альтернатива — дженерики (урок 13).
- **Typed-nil через функцию: func f() error { var e *MyErr = nil; return e }** — err != nil — TRUE в настоящем Go: return e упаковывает пару (*MyErr, nil), динамический тип заполнен. Вызывающий видит «ошибку» из ничего. Способ избежать — голый return nil явно в ветке успеха (пара (nil, nil)). Оба движка песочницы на этом сюжете отвечают неверно — проверять локальным go run.
- **fmt.Println(a ...any) — причём тут any** — Печать устроена на вариадике any: аргументы упаковываются в []any, fmt идёт циклом и форматирует каждый по динамическому типу; если тип реализует fmt.Stringer — зовётся String(). Своя версия: func printAll(vals ...any). Многоточие обязательно: один any — ровно один аргумент.

## Коротко: что запомнить

- Полиморфизм — вызов одного и того же метода у значений разных типов под одним контрактом: решение принимает пара «динамический тип + значение» внутри интерфейсной переменной.
- Срез интерфейсов []fmt.Stringer хранит упакованные пары; цикл зовёт метод легально, потому что компилятор сверил наборы методов при упаковке.
- any — пустой интерфейс: контракт из нуля методов, ему удовлетворяет всё; с Go 1.18 any — алиас старой формы interface{}.
- []any — срез разных типов: len/range/append работают, но у элементов нет ни одного метода — арифметика и вызовы не компилируются.
- fmt.Println(a ...any) устроен на вариадике any: fmt разбирает динамический тип каждого элемента и зовёт String() у Stringer-ов.
- Type safety — главный аргумент против any: пустой контракт выключает компиляторную проверку, ошибки уходят в рантайм; берите контракт из одного метода вместо any, если он существует.
- Typed-nil в производственной форме: func f() error { var e *MyErr = nil; return e } даёт err != nil TRUE (пара (*MyErr, nil)); лечение — голый return nil явно.
- Распаковка any — type switch и type assertion с ok-формой: территория двенадцатого урока; гибкость без потери type safety даёт дженерики — тринадцатый урок.

## Домашнее задание

Соберите any-кабинет. Первый шаг: функция show(v any) и вызовы с пятью типами — int, string, float64, bool и срезом []fmt.Stringer из десятого урока; убедитесь, что fmt печатает всё, и запишите в комментарий, почему внутри show нельзя вызвать v.String(). Второй шаг: box := []any{} — долейте append-ом значение каждого типа и напечатайте len; затем попробуйте box[0] + 1 и зафиксируйте текст ошибки компилятора. Третий шаг: функция printAll(vals ...any), которая печатает каждый элемент с нумерацией (fmt.Println(i+1, v)) — вариадик any как у fmt.Println. Четвёртый шаг: напишите badFetch и goodFetch из урока в отдельном файле, соберите локальным go build/go run и проверьте на настоящем Go, что badFetch(true) != nil, а goodFetch(true) == nil; перепишите badFetch на return nil. Комментарии в коде — по-русски.

## Вопросы и ответы по уроку

### Чем any отличается от interface{}?

Ничем, кроме имени: any — алиас, введённый в Go 1.18 для interface{} (пустой интерфейс — контракт из нуля методов). Компилятор разворачивает одно в другое, значения и сигнатуры полностью взаимозаменяемы. В коде до 1.18 встречается старая форма, в новом коде принят короткий алиас.

### Почему []any хуже []fmt.Stringer, если оба хранят разные типы?

Разница в гарантиях. []fmt.Stringer фиксирует контракт String() string: цикл может легально звать метод у каждого элемента. []any не гарантирует ни одного метода: элементы нельзя вызвать, сложить или сравнить — только печатать fmt-ом и перекладывать. Чем пустее контракт, тем больше типов влезает и тем меньше с ними можно сделать; выбирайте наименьший осмысленный контракт.

### Как правильно возвращать ошибку, чтобы не поймать typed-nil?

В ветке успеха пишите return nil дословно. Конструкция var e *MyErr = nil с последующим return e упаковывает пару (*MyErr, nil): динамический тип заполнен, и проверка err != nil у вызывающего даёт true — ошибка «есть», хотя внутри nil. Голый return nil кладёт пару (nil, nil), и проверка работает честно.

### Когда any действительно уместен?

Там, где функция специализируется на незнании: fmt.Println(a ...any), распаковка JSON в any при неизвестной структуре, слои, которые перекладывают данные, не вникая в содержание. Если же для значений существует контракт хотя бы из одного метода — берите его ([]fmt.Stringer вместо []any). Для «функции для многих типов» с сохранением type safety с Go 1.18 существуют дженерики.
