Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 11 из 25 ~20 мин +50 XP

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

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

Содержание урока · 14 разделов

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

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

В десятом уроке мы уже видели этот механизм в работе: срез []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-ом с ним делать нечего.

Вывод

Запустите код, чтобы увидеть вывод.

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 принимает всё.

Вывод

Запустите код, чтобы увидеть вывод.

[]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?

Прочитайте код и выберите, что он выведет:

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])
}

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 и убедитесь, что та же строка собирается — контракт вернул компилятору зрение.

Вывод

Запустите код, чтобы увидеть вывод.

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

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

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?

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-переменной

Прочитайте код и выберите, что он выведет:

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)
}

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

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

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 и его семья#

ИнструментЧто этоПравилоОсторожно
anyпустой интерфейс: контракт из нуля методов, ему удовлетворяет всёалиас interface{} с Go 1.18; старая форма — то же самоеу any нет методов: вызывать у значения нечего, компилятор слеп
[]anyсрез пар «динамический тип + значение» разных типовlen, range, append работают; смешивать типы легальноарифметика и методы над элементами не скомпилируются
func show(v any)универсальный параметр: принимает любой типзаконно для печати/сериализации/перекладываниявнутри функции с аргументом нельзя сделать ничего осмысленного
fmt.Println(a ...any)печать через вариадик anyfmt разбирает динамический тип; Stringer зовёт String() саммноготочие ...any обязательно: один any — один аргумент
type switchswitch v.(type): распаковка динамического типа по веткамв каждой case переменная конкретного типарезервный движок не поддерживает; детали — в двенадцатом уроке
typed-nil: return evar 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.

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

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

Поиск any interface golang теперь ведёт вас дальше по той же тропе. Следующие два шага закрывают оставленные хвосты. «Приведение типов и 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. Весь курс движется к проектированию API — с типизированными инструментами вместо пустых корзин.

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

Термин

Полиморфизм в Go — как достигается

нажмите, чтобы перевернуть
Определение

Значения разных конкретных типов под одним контрактом отвечают на один вызов по-разному: переменная интерфейсного типа хранит пару «динамический тип + значение», вызов уходит динамическому типу. Без наследования: новый тип с нужным методом сам вписывается в существующие срезы и циклы.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Полиморфизм — вызов одного и того же метода у значений разных типов под одним контрактом: решение принимает пара «динамический тип + значение» внутри интерфейсной переменной.
  • Срез интерфейсов []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. Комментарии в коде — по-русски.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Pro · Полиморфизм и пустой интерфейс)

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

Чем 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 существуют дженерики.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией