---
title: "Приведение типов и type assertion"
description: "Type assertion в Go: извлечение динамического типа из any, ok-форма без паники, type switch по типам, одиночная форма и паника, отличия от приведения чисел."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/12-type-assertion/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Приведение типов и type assertion

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

type assertion go — запрос, который вводят, когда any уже исчерпал гостеприимство: в одиннадцатом уроке мы упаковывали что угодно в пару «динамический тип + значение», но доставать значения обратно компилятор не давал — у any нет ни одного метода, и арифметика над коробкой не собирается. Двенадцатый урок — инструменты распаковки. Одиночная форма x.(T): «утверждаю, внутри T» — и паника interface conversion, если утверждение ложно. ok-форма v, ok := x.(T): тот же вопрос без риска, с флагом удачи и нулевым значением при неудаче. type switch: переключение по динамическому типу, в каждой ветке значение снова конкретного типа. И развилка, на которой путаются все: чем утверждение x.(T) отличается от приведения числовых типов T(b) — операций с похожим видом и совершенно разной природой. Все примеры запускаются прямо на странице.

### Type assertion в Go: как достать динамический тип из any

В [прошлом уроке](/courses/go-advanced/lessons/11-polimorfizm/) мы закончили честным признанием: `any` — коробка, из которой компилятор не даёт ничего достать. Положить можно всё, вынуть — ничего: `box[0] + 1` не собирается, `v.String()` не собирается, ошибку компилятор формулирует коротко — «у any нет методов». Стена нарочно высокая: она напоминает, что вы отдали компилятору зрение, когда упаковали значение в пару «динамический тип + значение».

В этом уроке мы возвращаем зрение — вручную, точкой и именем типа. Операция называется **type assertion**, утверждение типа, и записывается как `x.(T)`: после выражения-интерфейса ставится точка и в скобках тип-претендент. Читается дословно: «утверждаю, что динамический тип значения, лежащего в x, — это T; отдай мне его как T». Если утверждение верно — вы получаете значение в его настоящем типе, и у него снова работают все операции: арифметика, методы, поля. Если неверно — поведение зависит от формы записи, и здесь начинается главное правило урока: **у type assertion две формы, безопасная и опасная**, и выбирать между ними нужно осознанно.

Type assertion — единственный штатный способ распаковать интерфейсное значение в Go. Дженериков вплоть до версии 1.18 не было, any был единственной корзиной «всё», и весь код стандартной библиотеки распаковывал её именно утверждением типа. Поэтому `x.(T)` вы встретите везде: в парсерах JSON, в драйверах баз данных, в обработчиках ошибок, в middleware веб-фреймворков, где из `c.Request()` достают конкретный контекст.

План урока: сначала ok-форма — безопасный вопрос «внутри T?», затем одиночная форма и её паника, затем распаковка `[]any`-коробки из прошлого урока, проверка на интерфейсный тип и type switch — переключение по динамическому типу. Напоследок разграничим утверждение типа и приведение числовых типов — операции, которые новички постоянно путают из-за похожего вида. Интерактивы спроектированы под оба движка песочницы; где движки расходятся — честная оговорка.

### Анатомия x.(T): вопрос, заданный паре «динамический тип + значение»

Вспомним устройство из десятого урока: интерфейсная переменная — не контейнер с данными, а **пара из двух слов** — указателя на информацию о динамическом типе и указателя (или копии) самого значения. `var v any = 42` — пара (`int`, 42); `v = "семь"` — пара (`string`, "семь"). Компилятор при упаковке тип знает и проверяет; после упаковки он его **забывает** — в этом смысл any. Type assertion — это операция, которая заглядывает в первую половину пары и сверяет её с вашим заявлением.

Синтаксис: `x.(T)`, где `x` — выражение интерфейсного типа (any, error, fmt.Stringer, ваш собственный контракт), а `T` — тип-претендент: конкретный (`int`, `string`, ваша структура) или интерфейсный (об этом отдельно ниже). Результат операции — значение типа `T`, то есть после удачного утверждения вы работаете уже не с парой, а с конкретным значением: у него снова есть тип, операции, методы.

Форм две. **ok-форма** присваивает два значения: `n, ok := v.(int)` — первое это распакованное значение, второе — булев флаг удачи. Паники не бывает никогда: не совпало — получите нулевое значение T и `ok == false`. **Одиночная форма** присваивает одно значение: `n := v.(int)` — и верит вам на слово: если динамический тип не `int`, программа паникует с сообщением interface conversion. Первая форма — вопрос; вторая — заявление под ответственность.

Важная деталь, на которой спотываются: `x` обязан быть **интерфейсного** типа. Утверждение `s.(string)` над обычной строковой переменной не соберётся — компилятор и так знает её тип, утверждать нечего. Assertion — операция над парой, а пара есть только у интерфейсов. Отсюда и логика урока: упаковали через any — распаковывайте через assertion; у конкретных типов распаковывать нечего.

### ok-форма: спросить и не паниковать

Запустите: первый вопрос — к паре (int, 42) — удачен, ok равен true, n получил значение. Второй вопрос — про string — неудачен: ok false, а s стал пустой строкой, потому что при неудаче ok-форма кладёт в переменную нулевое значение претендента. Обратите внимание на запись: s, ok объявлены через := при первом использовании, а дальше присваиваются обычным равенством — переменные уже существуют. Эксперимент: спросите у v про bool и float64 и посмотрите на флаг — он честно отвечает false, не заставляя программу падать.

```go
package main

import "fmt"

func main() {
        var v any = 42

        // ok-форма: даже при неудаче паники не будет
        n, ok := v.(int)
        fmt.Println("int:", n, ok)

        s, ok := v.(string) // неудача: s — нулевая строка, ok — false
        fmt.Println("string ok?", ok, "| пустая?", s == "")

        v = "семь"
        s, ok = v.(string)
        fmt.Println("повтор:", s, ok)
}
```

### ok-форма при неудаче: нулевое значение и флаг

Разберём механику неудачи, потому что на ней построены все идиомы распаковки. Если динамический тип пары не равен `T`, ok-форма `v, ok := x.(T)` возвращает **два честных факта**: `ok == false` и `v` — нулевое значение типа `T`. Для `int` это 0, для `string` — пустая строка, для указателя — nil, для вашей структуры — структура с нулевыми полями. Нулевое значение — не «ошибка» и не «мусор»: это обычное корректное значение типа, которое вы обязаны игнорировать, пока флаг false. Отсюда железное правило: **читайте флаг до значения**. Идиома оформляется if-ом с инициализатором:

`if n, ok := v.(int); ok { fmt.Println("целое:", n) }`

Переменные `n` и `ok` живут только внутри ветки — компилятор не даст вам случайно использовать n за её пределами, где флаг не проверен. Это тот же приём, что `if err != nil` у функций: область видимости сужена до проверенного пути.

Отдельный случай — nil-источник. Если сама пара пуста (`var v any`), любой вопрос `v.(T)` в ok-форме отвечает `ok == false` — паники нет и здесь. Пустая пара не совпадает ни с каким типом, и ok-форма это фиксирует спокойно. Это делает ok-форму пригодной для обработки данных, которым вы не доверяете: входных JSON-ов, значений из внешних API, содержимого карт `map[string]any` — что бы ни лежало внутри, включая ничего, — программа не упадёт.

И тонкость, которую стоит проговорить: `ok == false` и «нулевое значение» — это **одна** ситуация, а не две. Не бывает «успеха с нулём» и «неудачи с нулём» одновременно — вопрос ок-формы всегда ровно один: совпал ли динамический тип. Поэтому нельзя полагаться на сравнение значения с нулём как на признак неудачи — единственный источник правды здесь флаг.

### Одиночная форма v.(T): законные случаи и паника interface conversion

Одиночная форма `n := v.(int)` — это заявление «я знаю, что внутри». Если заявление верно, форма короче и удобнее. Если нет — программа паникует: **interface conversion**. Настоящий Go формулирует панику исчерпывающе: `panic: interface conversion: interface {} is string, not int` — «в паре лежит string, а утверждали int». Сообщение читается как отчёт о преступлении: указан и настоящий динамический тип, и заявленный.

Когда одиночная форма **законна**? Тогда, когда динамический тип гарантирован логикой программы, а не надеждой. Три честных случая. Первый: вы сами только что упаковали значение и распаковываете его на соседней строке — тогда зачем вообще any, но если слои кода между упаковкой и распаковкой тип не теряли, утверждение верно. Второй: контракт функции обещает, что в any лежит конкретный тип, и это зафиксировано документацией и тестами — так устроены колбэки, где регистрация принимает `any`, а вызов знает, что передавал. Третий: выше по коду стоит проверка — type switch или ok-форма — и вы внутри проверенной ветки; правда, в этом случае идиома требует переиспользовать уже распакованное значение, а не утверждать повторно.

Анти-паттерн очевиден: «я уверен, что там int, проверять не буду». Уверенность в динамическом типе — это ставка на то, что все вызывающие во всех углах программы навсегда сохранят текущее поведение. Ставка проигрывает: рефакторинг меняет упаковщика, assertion в глубине роняет продакшн в три часа ночи. Паника interface conversion — не «дорога в аду», а честный сигнал, что где-то нарушение контракта между упаковкой и распаковкой; но сигнал дорогой, потому что срабатывает в рантайме, у пользователя.

Правило урока: **по умолчанию ok-форма; одиночная форма — осознанное исключение** с комментарием, откуда гарантия. В библиотечном коде паника иногда уместна как «невозможное состояние» — но тогда её стоит хотя бы задокументировать.

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

**Вопрос:** var v any = "не число"; n, ok := v.(int). Что произойдёт дальше?

- Паника interface conversion: программа упадёт
- ✅ n получит 0, ok станет false, программа продолжит работу
- Ошибка компиляции: string нельзя привести к int
- n получит «не число», ok станет true

ok-форма не паникует никогда. Динамический тип пары — string, а спрашивали int: несовпадение даёт ok == false и нулевое значение претендента, то есть 0 для int. Ошибки компиляции нет — типы пары и претендента компилятор не сверяет: он забыл динамический тип, вопрос задаётся в рантайме. Паника была бы у одиночной формы n := v.(int) — с сообщением interface conversion.

### Что выведет одиночная форма?

```go
package main

import "fmt"

func main() {
        var v any = "не число"
        n := v.(int)
        fmt.Println(n)
}
```

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

- Печатает 0: несовпадение даёт нулевое значение int
- ✅ Паника interface conversion: в паре string, а утверждали int
- Печатает «не число»: значение печатается как есть
- Не собирается: string и int — разные типы

Одиночная форма верит заявлению и не ставит флаг: динамический тип string против претендента int — паника interface conversion. Настоящий Go печатает «panic: interface conversion: interface {} is string, not int». Резервный движок песочницы сокращает формулировку до «interface conversion: string is not int», основной — совпадает с настоящим Go. Лекарство уже знакомо: ok-форма n, ok := v.(int) вместо паники даёт флаг false и нулевое значение.

### Распаковка коробки any из одиннадцатого урока: каскад ok-форм

Вернёмся к коробке `[]any` из прошлого урока — срезу, где в одной корзине лежали int, string и bool. Тогда мы могли её только печатать; теперь распакуем. План простой: идём циклом по срезу, для каждого элемента задаём серию ok-вопросов — «ты int?», «ты string?», «ты bool?» — и обрабатываем первый удачный. Идиома оформляется каскадом if-ов с инициализатором и `continue`:

`if n, ok := v.(int); ok { fmt.Println(n); continue }`

`continue` после обработки — важная деталь: элемент распакован, его вопрос закончен, идём к следующему. Без него каждый элемент проваливался бы во все последующие вопросы (безвредно — флаги false, — но расточительно и мутно).

Каскад ok-форм — рабочий инструмент, и в небольшом коде он честнее всего выглядит. У него есть два слабых места, о которых стоит знать заранее. Первое: **элемент, не совпавший ни с одним претендентом, молча пропускается** — float64 в коробке не напечатается вовсе; в данных, которым вы доверяете, молчаливый пропуск — потеря, поэтому ниже появится type switch с веткой default, которая ловит «всё остальное» громко. Второе: каскад растёт линейно с числом типов — пять типов, пять if-ов; type switch делает то же самое одной конструкцией. Но для двух-трёх типов каскад читается отлично и не требует выученного синтаксиса.

Третий нюанс каскада — порядок вопросов. Пары «динамический тип + значение» сравниваются с претендентом **точно**, поэтому порядок if-ов на результат не влияет: int не перепутается с int64, string с bool. Это не range-проверки, а точное равенство имён типов в паре — и к точности мы вернёмся, когда будем отличать assertion от преобразования.

### Восстановите каскад распаковки

Впишите по одному типу-претенденту в каждый пропуск: вопросы идут в порядке int, string, bool.

```go
package main

import "fmt"

func main() {
        box := []any{42, "семь", true}
        for _, v := range box {
                if n, ok := v.(___); ok {
                        fmt.Println("целое:", n)
                        continue
                }
                if s, ok := v.(___); ok {
                        fmt.Println("строка:", s)
                        continue
                }
                if b, ok := v.(___); ok {
                        fmt.Println("логическое:", b)
                }
        }
}
```

Подсказка: Каждый пропуск — имя типа-претендента внутри скобок утверждения: сначала целый, затем строковый, затем логический.

Правильные ответы в пропусках: `int`, `string`, `bool`

### Соберите ok-форму по шагам

Соберите программу: any со строкой внутри, ok-форма, печать значения после проверки флага.

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

1. `package main`
2. `import "fmt"`
3. `func main() {`
4. `var v any = "привет"`
5. `s, ok := v.(string)`
6. `if ok {`
7. `fmt.Println("строка:", s)`
8. `}`
9. `}`

### Проверка на интерфейсный тип: v.(fmt.Stringer) и урок песочниц

Претендент в `x.(T)` не обязан быть конкретным типом — им может быть **интерфейс**. Вопрос `s, ok := v.(fmt.Stringer)` звучит иначе, чем вопрос про int: не «ты строка?», а «умеешь ли ты печатать себя?». В настоящем Go удача означает: динамический тип пары реализует контракт целиком, и результат `s` — значение **интерфейсного** типа fmt.Stringer, готовое к вызову `s.String()`. Так проверяют не идентичность, а способность: у error-значения спрашивают `err.(*MyErr)` — «ты моя ошибка?», у любых значений — «ты Stringer?».

Именно ok-форма на error-пути — тот инструмент различения, который обещал одиннадцатый урок в истории с typed-nil. Напомнить связь одной фразой: ловушка была в том, что `var e *MyErr = nil; return e` даёт вызывающему пару (*MyErr, nil) и обманывает `err != nil`. Ok-форма `me, ok := err.(*MyErr)` — вежливый вопрос к interface-паре без паники: она различает «моя ошибка с полями» и «что-то чужое» не сравнением с nil, а проверкой динамического типа. В проде это главный способ достать свою структуру из generic-параметра error и прочитать её поля. Разбор самой ловушки не повторяем — он сделан в одиннадцатом уроке; здесь важно, что распаковка — её рабочее продолжение.

Теперь честный сюжет, ради которого тема интерфейсных претендентов не идёт в ранимые блоки. Наши обе песочницы на этом сюжете **врут**: код `var v any = Cat{...} с методом String(); s, ok := v.(fmt.Stringer)` обе отвечают `ok == false`, тогда как настоящий Go говорит true; одиночная форма `s := v.(fmt.Stringer)` паникует в обеих, хотя в настоящем Go проходит. Проверено пробниками до написания урока. Поэтому утверждение к интерфейсным типам разобрано здесь текстом и quiz-ом, а все ранимые блоки урока утверждают только к **конкретным** типам — на них оба движка точны. Сверяйте интерфейсные сюжеты локальным `go run`: это правило курса, и оно продолжает честность прошлых уроков.

### ok-форма и своя ошибка: спросить, прежде чем звать

```go
package main

import "fmt"

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

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

// sendInvite возвращает либо *MyErr, либо голый nil — как в 11-м уроке
func sendInvite(ok bool) error {
        if !ok {
                return &MyErr{msg: "гость не подтвердился"}
        }
        return nil
}

func main() {
        err := sendInvite(false)

        // ok-форма: «вежливый вопрос» к паре error — ты ведь *MyErr?
        if me, ok := err.(*MyErr); ok {
                fmt.Println("своя ошибка:", me.msg)
        } else {
                fmt.Println("не моя или пустая:", err)
        }

        err = sendInvite(true)
        if me, ok := err.(*MyErr); ok {
                fmt.Println("своя ошибка:", me.msg)
        } else {
                fmt.Println("не моя или пустая:", err)
        }
}
```

Ok-форма различает свою ошибку в обёртке error без паники: в первой ветке динамический тип пары — *MyErr, флаг true, и поля структуры доступны; во второй — пара (nil, nil), флаг false, и мы печатаем err как есть. Блок статичный намеренно: путь error с настоящей ошибкой обе песочницы проходят верно (проверено пробником), но на пути typed-nil из одиннадцатого урока обе врут, поэтому весь error-сюжет курса живёт в статичных блоках и сверяется локальным go run. Ошибки со сквозной упаковкой %w и распаковкой через errors.As — дело пятнадцатого урока.

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

**Вопрос:** У Cat есть метод String() string. В НАСТОЯЩЕМ Go: var v any = Cat{name: "Барсик"}; s, ok := v.(fmt.Stringer). Чему равно ok?

- false: у any нет методов, поэтому на Stringer v не тянет
- ✅ true: динамический тип пары — Cat, а Cat реализует контракт Stringer
- Программа паникует: interface conversion
- Не собирается: претендент не может быть интерфейсом

Претендент-интерфейс проверяет способность, а не идентичность: динамический тип Cat имеет метод String() string — контракт fmt.Stringer покрыт, ok равно true, s готов к вызову String(). Песочницы урока здесь врут (обе отвечают false, а одиночную форму паникуют) — потому этот сюжет вынесен в текст и quiz, а ранимые блоки утверждают только к конкретным типам. Проверьте локальным go run.

### Что выведет каскад с незнакомым элементом?

```go
package main

import "fmt"

func main() {
        box := []any{"голос", 7, 3.5, true}
        for _, v := range box {
                if n, ok := v.(int); ok {
                        fmt.Println("целое:", n)
                        continue
                }
                if s, ok := v.(string); ok {
                        fmt.Println("строка:", s)
                        continue
                }
                if b, ok := v.(bool); ok {
                        fmt.Println("логическое:", b)
                }
        }
        fmt.Println("готово")
}
```

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

- Четыре строки: все элементы распечатаются, включая 3.5
- ✅ Три строки и «готово»: float64 не совпал ни с одним претендентом и молча пропущен
- Паника на 3.5: тип не совпал ни с одним вопросом
- Не собирается: в []any нельзя положить float64

Каскад ok-форм — серия точных вопросов: строка и int и bool найдут свои ветки, а float64 не совпадёт ни с одним претендентом — все флаги false, элемент молча пропущен, паники нет (ok-форма). Молчаливый пропуск — главная слабость каскада: данных как будто не было. Type switch с веткой default решает это громко: незнакомый тип попадает в явную ветку «всё остальное». Именно поэтому в данных, которым вы не доверяете, предпочтительнее switch с default.

### Type switch: переключение по динамическому типу

Каскад ok-форм решает задачу «спросить про несколько типов», но у Go есть конструкция ровно для этого — **type switch**, переключение по динамическому типу. Синтаксис узнаёт обычный switch, но в скобках стоит не выражение, а утверждение с ключевым словом type:

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

Читается: «смотри на динамический тип пары, лежащей в v; если это int — работай в первой ветке, если string — во второй, иначе — в default». Главная прелесть конструкции — переменная `x`: **в каждой ветке она уже конкретного типа**. В `case int` компилятор видит int — легальна арифметика и %d; в `case string` — строка, легальна конкатенация; в default — x остаётся any. Это не «перегрузка» и не магия: switch разворачивается компилятором в каскад утверждений, и в каждой ветке x присваивается результат удачного `v.(тип_ветки)`. Ничего лишнего не напишете: внутри `case int` вызов строкового метода x не соберётся.

Перечислим свойства конструкции точно. Веток может быть сколько угодно, включая свои структуры — `case Cat:` легален. `default` необязателен, но без него незнакомые типы проходят мимо, как в каскаде. Можно писать без переменной — `switch v.(type)` — когда значение в ветке не нужно, а важен только факт типа. Несколько типов в одной ветке перечисляются через запятую — `case int, int64:` — тогда x в этой ветке остаётся any: компилятор не знает, которое из двух выпало. И ещё: порядок case-ов, в отличие от if-каскадов с диапазонами, для точных совпадений не важен — вопрос всегда «равен ли динамический тип этому претенденту», а не «попадает ли в диапазон».

Канонический вопрос: когда type switch, а когда ok-форма? Один тип-претендент — ok-форма; два и больше, особенно с веткой «всё остальное», — type switch: та же семантика, меньше букв, громче незнакомые типы.

### Type switch по трём типам и default

Запустите: четыре значения прошли через один switch — int поймал 42 и отформатировал %d, string склеил себя конкатенацией, bool распечатался через собственную ветку, а 3.14 провалился в default: динамический тип float64 не совпал ни с одним претендентом, и «всё остальное» прозвучало громко, без молчаливого пропуска. Эксперимент: допишите ветку case float64: return fmt.Sprintf("дробное %f", x) — и последний вызов перестанет попадать в default. Резервный движок песочницы конструкцию не исполняет — он отвечает «type switch (x.(type)) не поддерживается в песочнице; используйте type assertion x.(T)»; основной движок и локальный go run работают честно.

```go
package main

import "fmt"

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

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

### Приведение числовых типов: Т(b) — это не assertion

Пришло время развилки, на которой путаются даже опытные: чем `x.(T)` отличается от `T(b)`? Внешность почти совпадает — скобка с типом; природа противоположная. **Приведение типов** (conversion) `int(3.9)`, `float64(i)`, `string(rune)` — операция над **значением**: компилятор берёт биты числа и перетолковывает их в новом типе, меняя представление — срезая дробную часть, расширяя или сужая разрядность. Она происходит полностью на стадии компиляции, у неё нет ни «динамического типа», ни флага удачи, ни паники. Для любой пары числовых типов она легальна всегда: `int(3.9)` даст 3, `int64(i)` даст то же число в широком типе, `byte(300)` даст 44 — с усечением по модулю, без вопросов.

**Type assertion** `x.(T)` — операция над **парой**: она ничего не перетолковывает и не преобразует, а смотрит на динамический тип и требует **точного совпадения имён**. В паре лежит `int` — вопрос `x.(int64)` провалится, хоть значения и совместимы: int и int64 — разные динамические типы, пары (int, 42) и (int64, 42) — разные пары. Провал ok-формы — флаг false; провал одиночной формы — паника. Компилятор здесь бессилен: он забыл динамический тип, проверка — рантайм.

Отсюда практическая таблица решений. Значение уже есть, нужно другое представление (целое ↔ дробное, ширина разрядности) — преобразование `T(b)`: законно, компиляция, потеря точности на вашей совести. Значение лежит в интерфейсной паре и нужно обратно конкретным типом — assertion `x.(T)` с ok-формой. Между `int` и `int64` через any: сначала assertion к родному типу — `n, ok := x.(int)` — потом преобразование `int64(n)`, если нужно. Смешивать шаги нельзя: `x.(int64)` при паре (int, …) — ложь, а `int64(пара)` — вообще не собирается: преобразование не работает над интерфейсами, ему нужно значение.

И третья сила, живущая рядом: `strconv.Atoi("42")` — это ни то, ни другое, а **парсинг**: строка разбирается в число, ошибка приходит обычным err-ом. Если данные приходят текстом, вам нужен strconv, а не assertion — утверждение не умеет превращать строку в число, оно вообще ничего не превращает.

### Преобразование чисел против type assertion

Запустите: верхняя половина — преобразование: int срезал дробную часть 3.9 до 3, float64 вернул число в дробном представлении — оба шага молча легальны, ни флагов, ни паник. Нижняя половина — assertion: в паре лежит (int, 42), поэтому вопрос про int64 проваливается с ok false и нулевым значением 0, а вопрос про int удачен. int и int64 — разные динамические типы, хотя диапазон значений int64 покрывает int: assertion точен до имени типа. Эксперимент: добавьте строку wide := int64(n2) — преобразование после удачной распаковки: так выглядит правильный маршрут int → int64 через any.

```go
package main

import "fmt"

func main() {
        // ПРЕОБРАЗОВАНИЕ T(b): перетолковывает значение, компилируется всегда
        f := 3.9
        i := int(f) // срезает дробную часть: 3
        fmt.Println("int(3.9) =", i)
        d := float64(i)
        fmt.Println("float64(3) =", d)

        // ASSERTION x.(T): ничего не перетолковывает — сверяет динамический тип
        var v any = 42
        n, ok := v.(int64) // в паре int, а не int64: неудача!
        fmt.Println("int64 из int:", n, ok)
        n2, ok2 := v.(int)
        fmt.Println("int из int:", n2, ok2)
}
```

### interface{} и приведение типов golang: сводка четырёх инструментов

Соберём словарь урока в одну карту, потому что запрос **приведение типов golang** приводит на эту страницу сразу за несколькими словами, а в русском тексте «приведение типов» часто называют всё подряд: и conversion, и assertion, и парсинг. Разведём понятия окончательно.

**Преобразование (conversion)** — `T(b)` над значениями: числовые пары, именованные типы поверх базовых (`type Celsius float64` и обратно), строка в срез байт. Компиляторная операция, всегда легальна для совместимых представлений, потеря точности возможна, динамический тип не участвует.

**Утверждение (type assertion)** — `x.(T)` над интерфейсной парой: извлечение динамического типа. Две формы: ok-форма без паники и одиночная с interface conversion. Работает только с интерфейсами: у конкретных значений пары нет, утверждать нечего.

**Type switch** — `switch x := v.(type)`: синтаксическая упаковка серии утверждений, в каждой ветке x конкретного типа, default ловит незнакомцев.

**Парсинг** — `strconv`: текст в число и обратно, ошибка через error. Ни пара, ни преобразование тут не виноваты — это разбор формата.

Старая форма пустого интерфейса `interface{}` из прошлых уроков — тот же any: упаковка через него, распаковка через те же инструменты; исторически в старом коде вы встретите и `switch x := v.(type)`, и `v, ok := x.(int)` в паре с interface{} — правила полностью совпадают. И общий критерий выбора, наследуемый от одиннадцатого урока: чем раньше значение перестаёт быть any, тем лучше — **распаковывайте у границы** и дальше несите конкретный тип; assertion — мост из мира незнания в мир гарантий, и чем короче мост, тем дешевле проверка. Четыре инструмента, четыре задачи: не занимайте их чужую работу.

### Шпаргалка: инструменты вокруг пары «тип + значение»

| Инструмент | Что это | Правило | Осторожно |
| --- | --- | --- | --- |
| v, ok := x.(T) | ok-форма утверждения: вопрос к динамическому типу | паники нет никогда; при неудаче нулевое значение T и ok false | читайте флаг до значения; идиома — if с инициализатором |
| n := x.(T) | одиночная форма: заявление «внутри T» | только когда тип гарантирован логикой и это задокументировано | паника interface conversion: точные типы в сообщении |
| switch x := v.(type) | type switch: перебор претендентов с веткой default | в каждой ветке x конкретного типа; default — громкий улов | резервный движок песочницы не поддерживает |
| T(b) | преобразование значений: другое представление числа | компиляция всегда для числовых пар; int(3.9) — 3 | не смотрит на динамический тип; над интерфейсом не собирается |
| strconv.Atoi | парсинг строки в число | это не assertion: разбор формата, ошибка приходит err-ом | не путайте «достать тип» и «разобрать текст» |
| x.(*MyErr) | проверка на свой тип ошибки в обёртке error | ok-форма достаёт поля без паники; путь typed-nil — урок 11 | интерфейсные претенденты: песочницы врут, проверяйте go run |

### Правила гигиены type assertion

- По умолчанию ok-форма v, ok := x.(T); одиночная n := x.(T) — осознанное исключение с комментарием, откуда гарантия типа.
- Идиома — if с инициализатором (if n, ok := x.(int); ok {...}): распакованное значение не живёт за пределами проверенной ветки.
- Один претендент — ok-форма; два и больше — type switch с default: незнакомый тип должен попадать в громкую ветку, а не пропускаться молча.
- int и int64 — разные динамические типы: assertion между ними проваливается; нужен другой размер — сначала распакуйте родной тип, затем преобразуйте T(b).
- Преобразование T(b) — над значениями и всегда компилируется; assertion x.(T) — над парами и проверяется в рантайме: не подменяйте одно другим.
- Распаковывайте any у границы системы и несите дальше конкретный тип; утверждение к интерфейсным претендентам (Stringer, error-типы) сверяйте локальным go run — песочницы на нём врут.

> **Честно о двух движках песочницы.** Большинство интерактивов урока (ok-форма, каскад, преобразование против assertion, predict с каскадом, fill и order) дают одинаковый результат в обоих движках — основном (настоящий Go, yaegi в WASM) и резервном упрощённом. Расхождений четыре, и все проверены пробниками до написания урока. Первое: type switch резервный движок не исполняет — он отвечает «type switch (x.(type)) не поддерживается в песочнице; используйте type assertion x.(T)», поэтому switch-раннер проверяйте в основном движке или локальным go run. Второе: утверждение к ИНТЕРФЕЙСНОМУ претенденту (v.(fmt.Stringer)) обе песочницы проходят неверно — ok-форма отвечает false даже при совпадении (настоящий Go — true), а одиночная форма паникует; поэтому ранимые блоки утверждают только к конкретным типам, а интерфейсные сюжеты живут в тексте и quiz-е. Третье: одиночная форма к ПУСТОЙ паре — резервный движок не паникует и печатает nil, хотя настоящий Go и основной движок роняют программу с «interface conversion: interface {} is nil, not int»; nil-сюжет поэтому тоже только в тексте. Четвёртое: тексты паники одиночной формы различаются — резервный движок сокращает до «interface conversion: string is not int», основной совпадает с настоящим Go.

### Что дальше: дженерики забирают работу у any

Виток закрыт полностью: коробка any, которую [одиннадцатый урок](/courses/go-advanced/lessons/11-polimorfizm/) собрал, а [десятый](/courses/go-advanced/lessons/10-interfejsy/) объяснил на паре «динамический тип + значение», научилась открываться. Ok-форма `v, ok := x.(T)` — безопасный вопрос с флагом; одиночная `x.(T)` — заявление под ответственность с паникой interface conversion; type switch — перебор претендентов с громким default; преобразование `T(b)` — соседняя, но другая операция над значениями. Плюс инструмент различения своей ошибки `err.(*MyErr)`, продолжающий историю typed-nil. Контракт словаря прост: упаковка — по одну сторону точки, распаковка — по другую.

Что дальше по курсу? Тринадцатый урок — дженерики: type parameters и constraints. Это второй, более молодой ответ языка на ту же боль: «одна функция — много типов». Где any требовал распаковки assertion-ом и возвращал проверку в рантайм, дженерик `func Map[T, R any](s []T, f func(T) R) []R` держит типы в сигнатуре: компилятор знает их на этапе сборки, и ни пары, ни утверждения, ни паники не нужно. Двенадцатый урок дал инструменты распаковки, тринадцатый покажет, когда распаковка не нужна вовсе. Дальше — пакеты и модули ([весь курс](/courses/go-advanced/)) и ошибки со сквозной упаковкой, где ok-форма к `*MyErr` из сегодняшнего статичного блока вырастет в полноценные errors.Is и errors.As.

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

- **Type assertion x.(T) — что делает** — Заглядывает в первую половину интерфейсной пары «динамический тип + значение» и сверяет её с претендентом T. Совпало — возвращает значение в типе T (операции типа снова легальны). Работает только над интерфейсами: у конкретных значений пары нет.
- **ok-форма v, ok := x.(T) — поведение при неудаче** — Паники нет никогда: ok равно false, а v получает нулевое значение T (0, пустая строка, nil). Идиома — if с инициализатором: if n, ok := x.(int); ok {...}. Пустая пара (nil-интерфейс) тоже отвечает ok false. Читайте флаг до значения.
- **Одиночная форма n := x.(T) — когда законна** — Когда динамический тип гарантирован логикой: сами только что упаковали, контракт функции зафиксирован, выше стоит проверка. Иначе паника interface conversion с точным отчётом («interface {} is string, not int»). По умолчанию — ok-форма.
- **Type switch — синтаксис и главный бонус** — switch x := v.(type) { case int: ... case string: ... default: ... }. В каждой ветке x уже конкретного типа — легальны операции типа. Default ловит незнакомые типы громко, в отличие от молчаливого каскада ok-форм. Резервный движок песочницы не поддерживает — основной и go run работают.
- **Преобразование T(b) против assertion x.(T)** — Преобразование — операция над значением: перетолковывает биты (int(3.9) — 3), всегда компилируется, флагов и паник нет. Assertion — операция над парой: ничего не перетолковывает, требует точного совпадения имён типов: int из пары не достаётся как int64. Нужен другой размер — распакуйте родной тип, затем преобразуйте.
- **err.(*MyErr) — зачем и как** — Ok-форма различает свою ошибку в обёртке error без паники: me, ok := err.(*MyErr) — при удаче доступны поля структуры. Рабочее продолжение истории typed-nil из одиннадцатого урока. Путь с настоящей ошибкой обе песочницы проходят верно, путь typed-nil — врут обе: сверяйте локальным go run.

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

- Type assertion x.(T) — извлечение динамического типа из интерфейсной пары; работает только над интерфейсами, у конкретных значений распаковывать нечего.
- Ok-форма v, ok := x.(T) не паникует никогда: при несовпадении ok false, а v — нулевое значение T; идиома — if с инициализатором.
- Одиночная форма n := x.(T) паникует interface conversion при несовпадении; законна, когда тип гарантирован логикой, и это задокументировано.
- Каскад ok-форм распаковывает []any из прошлого урока; незнакомые типы он пропускает молча — type switch с default ловит их громко.
- Type switch switch x := v.(type) выбирает ветку по динамическому типу; в каждой ветке x конкретного типа; претендентом может быть интерфейс.
- Претендент-интерфейс проверяет способность, а не идентичность: v.(fmt.Stringer) в настоящем Go — ok true для Cat со String(); обе песочницы на этом врут — сверять go run.
- Преобразование T(b) — компиляторная операция над значением без паник; assertion — рантайм-проверка пары: int и int64 — разные динамические типы.
- err.(*MyErr) — ок-форма как инструмент различения своей ошибки, продолжение истории typed-nil; распаковывайте any у границы и несите конкретный тип дальше.

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

Соберите распаковщик заметок. Первый шаг: notes := []any{42, "текст заметки", true, 3.9} — пройдите циклом и распечатайте каждый элемент каскадом ok-форм (int/string/bool), убедитесь, что float64 молча пропущен; добавьте печать «незнакомый тип» через else в конце каскада и повторите. Второй шаг: перепишите каскад на type switch с веткой default — запустите локальным go run и сравните вывод. Третий шаг: функция sum(vals []any) int, которая складывает только int-элементы через ok-форму и игнорирует остальные; проверьте на срезе с примесями. Четвёртый шаг: повторите sendInvite и err.(*MyErr) из статичного блока в отдельном файле, соберите go build и убедитесь, что флаг ok работает на обоих вызовах; затем верните из функции (*MyErr)(nil) и проверьте, что err != nil — ловушка typed-nil из одиннадцатого урока, теперь различаемая ok-формой. Комментарии в коде — по-русски.

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

### Чем type assertion отличается от приведения типов (conversion)?

Assertion x.(T) работает над интерфейсной парой: он ничего не преобразует, а сверяет динамический тип с претендентом и извлекает значение; проверка происходит в рантайме, несовпадение даёт флаг false (ok-форма) или панику (одиночная форма). Приведение T(b) работает над значением: перетолковывает представление (int(3.9) — 3), всегда компилируется для числовых пар, динамического типа у него нет вовсе.

### Когда type assertion паникует и как этого избежать?

Паникует одиночная форма n := x.(T), если динамический тип пары не совпал с претендентом, а также при утверждении к пустой (nil) паре. Сообщение interface conversion называет оба типа. Избежать — ok-формой v, ok := x.(T): она никогда не паникует, при неудаче даёт ok false и нулевое значение T. Одиночная форма законна, когда тип гарантирован логикой программы.

### Type switch или ok-форма: что выбрать?

Один тип-претендент — ok-форма с if-инициализатором. Два и больше претендентов — type switch: та же семантика, но переменная ветки сразу конкретного типа и есть ветка default, которая ловит незнакомые типы громко; каскад ok-форм незнакомцев пропускает молча. Несколько типов через запятую в одной ветке допустимы, но переменная ветки остаётся any.

### Почему в паре лежит int, а x.(int64) даёт ok false?

Assertion сравнивает имена динамических типов точно: int и int64 — разные типы, пары (int, 42) и (int64, 42) различны. Assertion не преобразует размеры. Правильный маршрут: распаковать родной тип n, ok := x.(int), затем преобразовать int64(n). Преобразование, в отличие от assertion, свободно меняет ширину числового типа — но над интерфейсом оно не компилируется.
