voidlearn
 

1.1Типы и базовые конструкции

Разберём, что копирует присваивание и как тип задаёт допустимые операции. Затем свяжем zero value, переполнение, явные конверсии, константы и выравнивание структур с правилами языка.

Переменная, присваивание, указатель

Считай, что вся глава про один вопрос: если я где-то поменяю значение, увидит ли это изменение кто-то другой?

1. Переменная - это кусочек памяти с именем

Когда ты пишешь var x int64, программа откладывает в памяти кусочек размером 8 байт и вешает на него ярлык x. Размер кусочка задаёт тип: bool занимает 1 байт, int64 - 8, структура из двух int64 - 16. Компилятор знает размер заранее, ещё до запуска, поэтому и отводит ровно столько места, сколько нужно.

2. Присваивание - это копирование байтов из одного кусочка в другой

b := a означает буквально: «взять байты, которые лежат в кусочке a, и записать их в кусочек b». Два разных кусочка памяти, побайтовое копирование из первого во второй. С аргументом функции то же самое: у неё появляется свой кусочек памяти под параметр, и туда копируются байты из аргумента.

Отсюда и ответ на главный вопрос: раз кусочки разные, изменение одного не трогает другой. Загвоздка в одном: всё зависит от того, что именно лежало в скопированных байтах.

3. Указатель - переменная, в которой лежит не значение, а адрес

Иногда в кусочке памяти лежат не «полезные» данные, а адрес другого кусочка: число, которое говорит «нужное лежит вон там». Такая переменная называется указателем (тип пишется как *T, взять адрес - &x, сходить по адресу - *p).

Аналогия: значение - квартира, указатель - бумажка с её адресом. Скопировав бумажку, ты получил вторую бумажку, но квартира осталась одна. Придёшь по любой из двух бумажек, попадёшь в одну и ту же квартиру, и следы чужого ремонта увидишь по обеим.

Обычная переменная - копируются данные a 5 копия b 5 два разных кусочка памяти b = 5 → в a по-прежнему 5 Указатель - копируется адрес p 0x1040 копия q 0x1040 кусочек по адресу 0x1040 5 *q = 9 → через p тоже видно 9: адрес-то один Единственный вопрос, который всё это решает Поменяю копию - увидит ли оригинал? Зависит только от того, что лежало в скопированных байтах: сами данные или адрес.
Копия всегда происходит. Разница в том, что именно скопировали. Скопировали число - получили два независимых числа. Скопировали адрес - получили два указателя на одну и ту же память.

Система типов: что вообще есть

Все типы Go делятся на четыре группы, и отличаются они ровно одним: что лежит в том самом кусочке памяти, который скопируется при присваивании.

Группа Типы Что лежит в кусочке памяти Что будет при b := a
Базовые bool, числовые (int*, uint*, float*), byte, rune Само число или флаг - и всё Полностью независимая копия
Составные array, struct Все поля или элементы подряд, одним блоком Копируется весь блок целиком. Структура из 100 полей - скопируются все 100
С указателем внутри slice, map, chan, func, *T Маленькая служебная структурка, и внутри неё - адрес настоящих данных Копируется структурка с адресом. Данные остаются одни на двоих
Строки string Адрес байтов + длина. Сами байты лежат отдельно и неизменяемы Копируется адрес и длина, но менять строку всё равно нельзя - поэтому «одни на двоих» тут безопасно
Интерфейсы any, именованные интерфейсы Два адреса: «какой тут лежит тип» и «где само значение» Копируются оба адреса. Разбирается подробно в главе 1.5
Что сказать на собесе

В Go нет передачи по ссылке. Вообще. Присваиваешь или передаёшь в функцию: байты уезжают в новый кусочек памяти. Так происходит каждый раз, без исключений. Вопрос только в том, что было в этих байтах: само значение, и копия независима; адрес, и две переменные смотрят на одни и те же данные.

Заодно про жаргон. В книгах и на собесах говорят «значимые типы» и «ссылочные типы»: термины пришли из C# и Java. В спецификации Go их нет. Если интервьюер спрашивает «какие типы в Go ссылочные», сильный ответ звучит так: «формально таких нет, Go всегда копирует значение; но обычно ссылочными называют слайсы, мапы, каналы, функции и указатели, потому что внутри у них лежит адрес, и копия видит те же данные».

структура - копия целиком a X: 1 Y: 2 b X: 1 Y: 2 копия 16 байт b.X = 1 → a.X == 1 слайс - копия заголовка s1 ptr len cap s2 ptr len cap массив в куче заголовки разные - массив общий s2[0] = 9 → s1[0] == 9 мапа - копия указателя m1 ptr m2 ptr hmap buckets, count… два имени - одна хеш-таблица m2["k"]=9 → видно в m1 Общее правило Копируется всегда значение. Вопрос лишь в том, что это значение содержит: сами данные (struct, array) - или дескриптор с указателем на данные (slice, map, chan, func, interface).
Присваивание в Go. Структура копируется целиком, и значения выходят независимыми. У слайса и мапы копируется дескриптор, а данные остаются общими. Именно отсюда растут все «классические» вопросы про слайсы.

Целые числа: размер и переполнение

int и uint - платформозависимые типы: они шириной в машинное слово целевой архитектуры. На всех современных сборках (amd64, arm64) это 64 бита, на 32-битных (386, arm) - 32. Смотри strconv.IntSize или unsafe.Sizeof(int(0)). При этом int, int64 и int32 - это три разных типа, между которыми нет неявного приведения, даже когда их размеры совпадают.

var a int = 5
var b int64 = 10
// _ = a + b            // ошибка компиляции:
//                      // invalid operation: a + b (mismatched types int and int64)
_ = int64(a) + b        // ок - явная конверсия

// размер int зависит от платформы:
fmt.Println(strconv.IntSize)          // 64 на amd64/arm64
fmt.Println(unsafe.Sizeof(int(0)))    // 8

Переполнение целых в Go не паникует и не проверяется в рантайме: это арифметика по модулю 2^N («заворачивается»). Знаковые прыгают с максимума на минимум, беззнаковые с нуля на максимум.

var i8 int8 = 127
i8++                       // -128, без паники

var u uint8 = 0
u--                        // 255

// В проде чаще всего подводит беззнаковая арифметика:
var n uint = 3
for i := n; i >= 0; i-- {  // бесконечный цикл: uint никогда не станет < 0
    _ = i
}

// И вторая: разность длин в uint
a, b := len(x), len(y)     // len возвращает int, тут всё ок
var la, lb uint = 2, 5
_ = la - lb                // 18446744073709551613, а не -3
Подвох, который любят задавать

«Где здесь бесконечный цикл?» И показывают for i := uint(n); i >= 0; i--. Ответ: беззнаковое всегда >= 0, условие никогда не станет ложным. Второй вариант того же вопроса: for i := 0; i < len(s)-1; i++. Если s пустой, то len(s)-1 == -1, а int знаковый, так что цикл просто не выполнится. А будь длина беззнаковой, полезли бы по огромному индексу.

Компилятор ловит переполнение только у констант, потому что константы вычисляются с произвольной точностью на этапе компиляции:

const big = 1 << 62         // ок, нетипизированная константа: побитовый сдвиг "1 << 62" даёт 2⁶²
var x int8 = 200            // ошибка компиляции: constant 200 overflows int8
var y = big * 4             // ошибка: constant overflows int (при присвоении в int)

Zero values: почему в Go нет мусора в памяти

Любая объявленная переменная гарантированно инициализирована. Рантайм обнуляет выделенную память (memclr), и на стеке, и в куче. Это решение сознательное: оно убирает целый класс багов «использование неинициализированной переменной», делает var mu sync.Mutex сразу готовым к работе, а var buf bytes.Buffer - валидным. Формулировка для собеса: в Go нулевое значение должно быть полезным (zero value is useful), это принцип дизайна стандартной библиотеки.

Тип Zero value Можно ли пользоваться сразу
bool false да
числовые 0 / 0.0 да
string "" (не nil!) да, len == 0
*T, func, interface nil нет - разыменование/вызов паникует
slice nil да частично: len, cap, range, append работают
map nil только чтение: чтение даёт zero value, запись - паника
chan nil нет - чтение и запись блокируются навсегда
array массив из zero values да
struct структура, все поля обнулены да (если так спроектирована)
Мини-таблица «nil, но разный»

Слово nil в Go не обозначает один объект. Это нулевое значение для шести разных категорий типов, и ведут они себя по-разному. Классический добивающий вопрос: «а nil слайс и nil мапа одинаково себя ведут?» Нет: в nil-слайс можно append, в nil-мапу нельзя писать.

byte и rune

Оба - псевдонимы, не самостоятельные типы: type byte = uint8 и type rune = int32 объявлены через =, то есть это буквально одно и то же имя для одного типа. Поэтому byte и uint8 взаимозаменяемы без конверсии, а вот type MyByte uint8 (без =) создал бы новый тип, требующий явного приведения.

  • byte - один октет. Из них состоят строки, содержимое []byte, элементы I/O.
  • rune - одна кодовая точка Unicode, 32 бита. Это не «символ», каким его видит человек: составные эмодзи и буквы с диакритикой могут состоять из нескольких рун.
s := "Привет"
fmt.Println(len(s))                       // 12 - байты, кириллица по 2 байта в UTF-8
fmt.Println(utf8.RuneCountInString(s))    // 6  - руны
fmt.Printf("%T %v\n", s[0], s[0])         // uint8 208 - байт, не буква
fmt.Printf("%T %c\n", []rune(s)[0], []rune(s)[0]) // int32 П

Приведение типов: почему всё явно

В Go есть конверсия (T(v)) и нет неявного приведения между разными типами, даже если базовое представление у них одинаковое. Причина: код предсказуем, скрытых потерь нет. В C/C++/Java правила неявных промоушенов занимают страницы спецификации и регулярно рождают баги вида «поделили int на int и получили 0», «сравнили signed с unsigned». Go эту область просто удалил.

type UserID int64
type OrderID int64

var u UserID = 7
var o OrderID = u          // ошибка компиляции: cannot use u (variable of int64
                           // type UserID) as OrderID value in variable declaration
                           // ради этого отдельные типы и заводят
var o2 OrderID = OrderID(u) // явно, на ревью сразу видно

// Конверсия может молча терять данные:
var big int32 = 300
var small int8 = int8(big)  // 44, старшие биты отброшены, ошибки нет

// Конверсия float→int обрезает, а не округляет:
f := 2.99
fmt.Println(int(f))         // 2
fmt.Println(int(-f))        // -2
// Но только для переменной. int(2.99) от нетипизированной константы -
// ошибка компиляции: cannot convert 2.99 (untyped float constant) to type int
Конверсия ≠ type assertion

T(v) - конверсия, работает на конкретных типах, проверяется компилятором. v.(T) - type assertion, работает только с интерфейсными значениями и проверяется в рантайме. Путать их на собесе - заметный минус.

Константы и iota

Константы в Go вычисляются на этапе компиляции и бывают типизированными и нетипизированными. Нетипизированная константа хранится с произвольной точностью (минимум 256 бит по спецификации) и получает тип только в момент использования. Поэтому и можно писать math.MaxInt64 и 1 << 62 без гимнастики с приведениями.

const Pi = 3.14159           // нетипизированная float-константа
const TypedPi float64 = 3.14 // типизированная

var f float32 = Pi           // ок: нетипизированная подстроится
// var g float32 = TypedPi   // ошибка компиляции: cannot use TypedPi (constant
//                           // 3.14 of type float64) as float32 value

const Huge = 1 << 100        // ок, пока не присваиваем в переменную
fmt.Println(Huge >> 98)      // 4 - вычислено компилятором

iota - счётчик внутри блока const. Он равен индексу строки (ConstSpec) в блоке, начиная с нуля, и сбрасывается на каждом новом const (. Тонкость: если у строки нет своего выражения, повторяется выражение предыдущей строки. Так и работают «пустые» строки в перечислении.

type Weekday int

const (
    Sunday Weekday = iota // 0
    Monday                // 1 - выражение унаследовано
    Tuesday               // 2
)

// Пропуск значений через _
const (
    _  = iota             // 0 отбрасываем
    KB = 1 << (10 * iota)  // 1 << 10 = 1024
    MB                    // 1 << 20
    GB                    // 1 << 30
)

// Битовые флаги
type Perm uint8
const (
    Read Perm = 1 << iota // 1
    Write                 // 2
    Exec                  // 4
)
p := Read | Write
fmt.Println(p&Write != 0) // true
Чем опасен iota в проде

Значения привязаны к порядку строк. Вставили новую константу в середину - все последующие значения сдвинулись. Если эти числа попадают в БД, в API или в сообщения брокера, это тихая порча данных. Правило: для внешне видимых перечислений задавай значения явно либо добавляй новые только в конец, и обязательно закрывай тестом (например, golden-тест на String() из stringer).

switch: чем отличается от C-подобных языков

Отличие Как в Go
Провал в следующий case Нет по умолчанию. Есть явный fallthrough (передаёт управление в тело следующего case без проверки его условия)
Тип выражения Любой сравнимый тип, включая string, а не только целые
Несколько значений в case case 1, 2, 3:
Switch без выражения switch { case x > 10: ... } - заменяет цепочку if/else if
Инициализатор switch v := f(); v { - v живёт только внутри switch
Type switch switch v := i.(type) { - работает только с интерфейсами
break Не нужен для выхода из case. Нужен, чтобы прервать цикл извне switch - тогда с меткой
// break внутри switch выйдет из switch, а не из цикла, тут часто ошибаются
loop:
for _, ev := range events {
    switch ev.Kind {
    case KindStop:
        break loop        // без метки прервали бы только switch
    case KindSkip:
        continue loop
    }
    handle(ev)
}

// switch без выражения идиоматично заменяет лестницу if
switch {
case age < 13:
    return "child"
case age < 20:
    return "teen"
default:
    return "adult"
}

Структуры: сравнение и выравнивание

Структура comparable, если comparable все её поля. Не comparable: слайсы, мапы, функции, а значит и любая структура, которая их содержит. Массивы comparable, если comparable их элементы. Структуры сравниваются пополево, а не побайтово (иначе padding-байты давали бы ложные различия).

type Point struct {
    X, Y int
}
type Bag struct {
    Items []string
}

fmt.Println(Point{1, 2} == Point{1, 2}) // true
// _ = Bag{} == Bag{}                   // ошибка компиляции: invalid operation:
//                                      // Bag{} == Bag{} (struct containing
//                                      // []string cannot be compared)

// Обходной путь для «глубокого» сравнения:
reflect.DeepEqual(a, b)      // медленно, но работает со всем
cmp.Equal(a, b)              // github.com/google/go-cmp - стандарт де-факто в тестах

Выравнивание. Процессор читает память словами, поэтому компилятор кладёт каждое поле по адресу, кратному его выравниванию (int64 - по 8, int32 - по 4, bool - по 1), а между полями вставляет padding. Размер всей структуры дополняется до кратного максимальному выравниванию полей, чтобы в массиве структур каждый следующий элемент тоже был выровнен. Go не переупорядочивает поля сам: порядок в исходнике = порядок в памяти.

Bad type Bad struct { a bool; b int64; c bool } → 24 байта a padding 7 Б b - int64 c padding 7 Б Good type Good struct { b int64; a bool; c bool } → 16 байт b - int64 a c padding 6 Б 0 8 16 24 Правило укладки: поля от «широких» к «узким». Экономия 33 % на структуре, которой в проде миллион экземпляров, - это реальные мегабайты и меньше работы GC. Проверяется через structlayout / fieldalignment из go vet.
Padding. Один и тот же набор полей занимает 24 или 16 байт, и решает всё порядок объявления. Линтер fieldalignment находит такие структуры автоматически.
$ go vet -vettool=$(which fieldalignment) ./...
$ fieldalignment -fix ./...        # умеет переставлять поля автоматически
$ go run honnef.co/go/tools/cmd/structlayout@latest -json pkg Bad

Анонимные структуры и функции

// 1. Табличные тесты, самый частый случай
tests := []struct {
    name string
    in   string
    want int
    err  bool
}{
    {"пусто", "", 0, true},
    {"обычный", "42", 42, false},
}

// 2. Одноразовый JSON-ответ, ради которого нет смысла заводить тип
json.NewEncoder(w).Encode(struct {
    Status string `json:"status"`
    Count  int    `json:"count"`
}{"ok", n})

// 3. Множество (set) с нулевым оверхедом на значение
seen := map[string]struct{}{}
seen["a"] = struct{}{}

// 4. Анонимные функции: замыкания, defer, горутины, функциональные опции
defer func() {
    if r := recover(); r != nil { log.Println("recovered:", r) }
}()

Пустая структура struct{} занимает 0 байт, а все её экземпляры лежат по одному адресу (runtime.zerobase). Отсюда идиомы map[T]struct{} для множества и chan struct{} для сигнального канала: памяти под значение не выделяется вообще.

Вопросы

10
Суть: в Go всё копируется по значению; «ссылочность» значит лишь, что внутри скопированного значения лежит указатель.

Четыре группы типов:

  • Базовые - bool, строки, числовые (int, uint, float, complex), их псевдонимы byte и rune.
  • Составные - array и struct: фиксированный размер, элементы лежат подряд.
  • Дескрипторные (их и называют «ссылочными») - slice, map, chan, func, указатель *T.
  • Интерфейсные - any и именованные интерфейсы: два слова (тип + данные).

Присвоил или передал в функцию: значение копируется побитово. Для struct и array копируются все данные, и получаются два независимых объекта. Для слайса копируется заголовок из трёх слов (ptr/len/cap), для мапы и канала - указатель на hmap/hchan, поэтому данные остаются общими.

type P struct {
    X int
}
a := P{1}; b := a; b.X = 9
// a.X == 1 - копия независима

s1 := []int{1, 2, 3}; s2 := s1; s2[0] = 9
// s1[0] == 9 - общий массив

arr1 := [3]int{1, 2, 3}; arr2 := arr1; arr2[0] = 9
// arr1[0] == 1 - массив копируется целиком
Что добавить, чтобы ответ звучал сильно

«Термина reference type в спецификации Go нет, это жаргон. Формально Go всегда передаёт по значению, а изменяемость через слайс или мапу - следствие того, что в скопированном дескрипторе лежит указатель. Единственный способ дать функции изменить саму переменную вызывающего - передать *T.»

Суть: int - платформозависимый (64 бита на amd64/arm64), и это отдельный тип от int64, даже когда размеры совпадают.

Размер int/uint/uintptr совпадает с машинным словом: 8 байт на amd64, arm64 и wasm, 4 байта на 386 и arm. Проверить: strconv.IntSize или unsafe.Sizeof(int(0)).

Смешивать нельзя даже при равном размере: компилятор потребует явной конверсии. Совместимость определяет тип, а не представление. На практике: в протоколах и БД фиксируй int64/int32, а int оставь для индексов, длин и внутренних счётчиков.

Переполнение

Ни знаковое, ни беззнаковое переполнение не паникует: это арифметика по модулю 2^N. Компилятор ловит только константное.

var i int8 = 127; i++            // -128
var u uint8 = 0;  u--            // 255

// Ловушка №1: обратный цикл по uint
for i := uint(3); i >= 0; i-- {  // бесконечный: uint всегда >= 0
}

// Ловушка №2: разность беззнаковых
var a, b uint = 2, 5
fmt.Println(a - b)               // 18446744073709551613

// Ловушка №3: проверка переполнения после операции
c := math.MaxInt64
c++                              // уже -9223372036854775808
if c < 0 { /* поздно, но так обычно и ловят */ }

Как правильно проверять переполнение

// знаковые - руками:
func addSafe(a, b int64) (int64, error) {
    if (b > 0 && a > math.MaxInt64-b) || (b < 0 && a < math.MinInt64-b) {
        return 0, errors.New("overflow")
    }
    return a + b, nil
}
// сейчас для беззнаковых есть math/bits:
sum, carry := bits.Add64(x, y, 0)   // carry == 1 при переполнении
Отдельный вопрос-добивка

«Почему len() возвращает int, а не uint, ведь длина не бывает отрицательной?» Затем, чтобы выражения вида len(s)-1 и обратные циклы вели себя предсказуемо. Роб Пайк объяснял это так: беззнаковые типы в арифметике приносят больше багов, чем экономят диапазона. uint в Go - для битовых операций и протоколов, а не для «неотрицательных чисел».

Суть: рантайм обнуляет всю выделяемую память, поэтому у любой переменной сразу есть значение. Дизайн-принцип - «make the zero value useful».

0 для чисел, false для bool, "" для строк, nil для указателей, функций, интерфейсов, слайсов, мап и каналов; для массивов и структур - рекурсивно нулевые элементы/поля.

Почему так сделано

  • Убирает класс UB из C: чтение мусора со стека невозможно.
  • Можно проектировать типы, готовые к работе «из коробки»: var mu sync.Mutex, var buf bytes.Buffer, var wg sync.WaitGroup - все валидны без конструктора.
  • Упрощает JSON/десериализацию: отсутствующее поле = нулевое значение.

Цена и подводные камни

  • Неотличимо «не задано» от «задано нулём». Классика: omitempty выбросит false и 0, хотя пользователь их явно прислал. Помогают указатели (*bool) или sql.NullXxx / собственный тип с флагом.
  • nil-мапа читается, но не пишется: паника assignment to entry in nil map.
  • nil-канал блокирует навсегда (иногда это полезно: так «выключают» ветку в select).
var m map[string]int
fmt.Println(m["x"], len(m))  // 0 0 - чтение ок
m["x"] = 1                   // panic: assignment to entry in nil map

var s []int
s = append(s, 1)             // ок: append работает и с nil-слайсом

var ch chan int
<-ch                         // висит навсегда (deadlock, если спят все горутины)
Суть: это алиасы: byte = uint8, rune = int32. byte - единица хранения, rune - единица смысла (кодовая точка Unicode).

Объявлены через =, то есть это те же самые типы под другим именем, а не новые типы. Поэтому var b byte = uint8(5) компилируется без конверсии.

Go хранит строки в UTF-8. По индексу получишь байт, а range по строке выдаст руну и её байтовое смещение. Это главный источник ошибок с не-ASCII текстом.

s := "Go♥"
fmt.Println(len(s))                    // 5: 'G','o' по 1 байту, '♥' - 3 байта
fmt.Printf("%T\n", s[0])               // uint8
for i, r := range s {
    fmt.Printf("%d:%c(%d) ", i, r, r)  // 0:G(71) 1:o(111) 2:♥(9829)
}
fmt.Println(utf8.RuneCountInString(s)) // 3
rs := []rune(s)                        // аллоцирует 3 × 4 = 12 байт
fmt.Println(len(rs))                   // 3
Rune ≠ «символ»

Руна - это code point, а не grapheme cluster. Флаг «🇷🇺» - две руны, «é» может быть одной руной (U+00E9) или двумя (e + U+0301). Для «символов как их видит человек» нужна итерация по графемным кластерам, например библиотекой rivo/uniseg, а golang.org/x/text/unicode/norm только приводит «é» к одной форме. На собесе достаточно этот нюанс упомянуть: видно, что ты понимаешь границы модели.

Суть: неявных приведений нет вообще. Цель - убрать скрытые потери данных и сделать код читаемым без знания таблицы промоушенов.

Единственное «неявное» поведение - нетипизированные константы, которые подстраиваются под контекст. Всё остальное требует T(v).

Аргументы, которые стоит назвать

  1. Читаемость. Глядя на int64(x) * y, ты сразу видишь и стоимость, и намерение. В C всё это прошло бы молча.
  2. Именованные типы как защита. type UserID int64 и type OrderID int64 нельзя перепутать, компилятор поймает. С неявными приведениями от этой защиты ничего не остаётся.
  3. Нет signed/unsigned сюрпризов. В C при сравнении int с unsigned первое продвигается в беззнаковое, отсюда классические уязвимости.
  4. Простота спецификации. Меньше правил - меньше расхождений между компиляторами и меньше кейсов на ревью.

Что конверсия всё же делает молча

// От константы такие конверсии не скомпилируются,
// компилятор ловит их ещё при сборке:
//   int8(300)   → constant 300 overflows int8
//   int(2.99)   → cannot convert 2.99 (untyped float constant) to type int
//   uint(-1)    → constant -1 overflows uint

// А вот от переменной те же конверсии проходят молча:
var a int32 = 300
var b float64 = 2.99
var c int = -1

fmt.Println(int8(a))   // 44   - старшие биты отрезаны молча
fmt.Println(int(b))    // 2    - обрезает, а не округляет
fmt.Println(int(-b))   // -2   - к нулю, а не «вниз»
fmt.Println(uint(c))   // 18446744073709551615

string(65)         // "A"  - go vet ругается: подозрительная конверсия int→string
string([]byte{65}) // "A"  - а так правильно
Три разных механизма - не путать
  • T(v) - конверсия, компайл-тайм, конкретные типы.
  • i.(T) - type assertion, рантайм, только интерфейсы.
  • switch v := i.(type) - type switch, рантайм, ветвление по динамическому типу.
Суть: iota - индекс строки внутри блока const, считает с 0 и сбрасывается на каждом новом блоке. Пропущенное выражение повторяется с предыдущей строки.
const (
    A = iota      // 0
    B             // 1  - выражение "iota" унаследовано
    C             // 2
)
const (
    D = iota      // 0 - новый блок, счётчик сброшен
)

// iota считает строки, а не константы:
const (
    X, Y = iota, iota * 10   // X=0, Y=0
    Z, W                     // Z=1, W=10
)

Идиомы

// 1) Перечисление статусов, строки для них сгенерирует stringer
type Status int
const (
    StatusUnknown Status = iota
    StatusPending
    StatusDone
)
//go:generate stringer -type=Status

// 2) Размеры
const (
    _  = iota
    KB = 1 << (10 * iota)   // 1024
    MB                       // 1048576
)

// 3) Битовые маски
type Flag uint
const (
    FlagRead Flag = 1 << iota
    FlagWrite
    FlagExec
)

// 4) Пропуск ненужных значений
const (
    _ = iota
    First   // 1 - чтобы zero value означал "не задано"
    Second
)
Приём, который стоит показать

Начинай перечисление с _ = iota или заводи явный StatusUnknown = 0, чтобы нулевое значение означало «не заполнено», а не какой-то легальный статус. Иначе забытое поле в структуре молча становится валидным первым статусом. Типичный баг на проде.

Главный минус

Значения зависят от порядка строк. Если константы уезжают в БД/API/Kafka, вставка новой строки в середину меняет смысл уже сохранённых данных. Для внешних контрактов значения задают явно.

Суть: нет неявного fallthrough, работает с любыми сравнимыми типами, может быть без выражения (замена if/else) и умеет ветвиться по типу.
  1. Нет провала. Тело case отработало, switch закончился. Явный fallthrough передаёт управление в тело следующего case, не проверяя его условие, и обязан быть последним оператором в case.
  2. break не нужен для выхода из case. Внутри цикла break выйдет из switch, а чтобы прервать цикл, нужна метка.
  3. Любые типы: строки, структуры (если comparable), не только целые.
  4. Несколько значений в одном case: case "a", "b":.
  5. Switch без выражения - эквивалент switch true, идиоматичная замена лестнице if / else if.
  6. Инициализатор: switch v, err := f(); {, область видимости ограничена switch.
  7. Type switch: switch v := i.(type) - только по интерфейсам. Внутри каждого case v получает свой конкретный тип; в case с несколькими типами v остаётся интерфейсом.
  8. Порядок проверки - сверху вниз, первый совпавший case побеждает (в отличие от C, где switch может компилироваться в jump table и порядок не важен).
switch v := x.(type) {
case nil:
    fmt.Println("nil интерфейс")
case int, int64:
    fmt.Printf("целое, но v тут ещё any: %T\n", v)
case fmt.Stringer:
    fmt.Println(v.String())   // v уже fmt.Stringer
default:
    fmt.Printf("%T\n", v)
}

switch {                      // без выражения
case n < 0:  return "neg"
case n == 0: return "zero"
default:     return "pos"
}

switch n {
case 1:
    fmt.Println("один")
    fallthrough               // безусловно выполнит тело case 2
case 2:
    fmt.Println("два")
}
// при n == 1 вывод:
// один
// два
Что выведет?
for i := 0; i < 3; i++ {
    switch i {
    case 1:
        break            // выходит из switch, НЕ из цикла
    }
    fmt.Print(i)
}

012. Чтобы прервать цикл, нужен break label.

Суть: константы существуют только на этапе компиляции, у них нет адреса, а нетипизированные константы хранятся с произвольной точностью и получают тип в момент использования.

Отличия от переменных

  • Вычисляются компилятором; в бинарнике их «нет», они подставлены в места использования.
  • Нельзя взять адрес: &MaxSize - ошибка компиляции.
  • Могут быть только базовых типов: булев, строковый, числовой. Нельзя const s = []int{1} или const t = time.Now().
  • Не участвуют в escape analysis и не создают аллокаций.

Нетипизированные константы

Константа без явного типа хранится как «идеальное» значение произвольной точности (спецификация требует минимум 256 бит мантиссы) и приобретает тип только там, где используется: по правилу «тип по умолчанию» или по контексту.

const c = 1 << 100          // ок, пока не присвоено переменной
fmt.Println(c >> 98)        // 4

const half = 1 / 2          // 0 - целочисленное деление констант
const halfF = 1.0 / 2       // 0.5 - одна из констант вещественная

var f float64 = 3           // 3 подстроится под float64
var d time.Duration = 5     // ок: нетипизированная 5 станет Duration
var n int = 5
// var d2 time.Duration = n // ошибка: int → Duration неявно нельзя

// Типы по умолчанию, если контекста нет:
x := 42        // int
y := 4.2       // float64
z := 'a'       // rune (int32)
s := "hi"      // string
b := true      // bool
Практическая польза, которую редко называют

Из-за нетипизированных констант time.Second * 5 работает, а time.Second * n (где n int) - нет. И поэтому math.MaxUint64 можно объявить константой, хотя она не влезает в int64: до присваивания у неё просто нет типа.

Суть: структура comparable, если comparable все поля (нет слайсов, мап, функций). Сравнение идёт поле за полем. Порядок полей влияет на размер из-за выравнивания, и Go поля не переставляет.

Comparable

  • Comparable: числа, строки, bool, указатели, каналы, интерфейсы, массивы из comparable, структуры из comparable.
  • Не comparable: slice, map, func и всё, что их содержит. Сравнение с nil для них разрешено, друг с другом - нет.
  • Только comparable-типы могут быть ключами мапы, это то же ограничение.
type A struct {
    X int
    S string
}        // comparable
type B struct {
    Items []int
}            // не comparable
_ = A{1,"a"} == A{1,"a"}                // true
// _ = B{} == B{}                       // compile error

// С интерфейсным полем структура comparable, но в рантайме может запаниковать:
type C struct {
    V any
}
_ = C{1} == C{1}                        // true
_ = C{[]int{1}} == C{[]int{1}}          // panic: comparing uncomparable type []int

Выравнивание

Каждое поле кладётся по адресу, кратному его выравниванию (обычно = размеру: int64→8, int32→4, bool→1). Между полями появляется padding. Размер структуры округляется вверх до кратного максимальному выравниванию её полей, чтобы в []T все элементы были выровнены.

type Bad struct {           // 24 байта
    a bool                  // 1 + 7 padding
    b int64                 // 8
    c bool                  // 1 + 7 padding (добивка до кратного 8)
}
type Good struct {          // 16 байт
    b int64                 // 8
    a bool                  // 1
    c bool                  // 1 + 6 padding
}
unsafe.Sizeof(Bad{})        // 24
unsafe.Sizeof(Good{})       // 16
unsafe.Alignof(Bad{})       // 8
unsafe.Offsetof(Bad{}.b)    // 8

Когда это реально важно

  • Структура живёт миллионами экземпляров (кэш, индекс в памяти, элементы большого слайса). Тогда −33 % размера дают и меньше давления на GC, и лучшую локальность в L1/L2.
  • False sharing в конкурентном коде: два часто изменяемых поля попадают в одну 64-байтовую кэш-линию, ядра начинают гонять её друг у друга. Лечат вручную: padding или [64]byte-заполнитель между полями.
Практика

Клади поля от «широких» к «узким»: указатели и int64 → int32 → int16 → bool. Такие структуры находит линтер fieldalignment (входит в golangci-lint как часть govet), он же их чинит флагом -fix. Но не начинай ответ с этого: сначала измерь, потом переставляй поля.

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

Анонимные структуры

  • Табличные тесты - самый частый и самый идиоматичный случай.
  • Одноразовый JSON на входе или выходе хендлера, когда тип не нужен в домене.
  • Множества: map[string]struct{}, где значение занимает 0 байт.
  • Сигнальные каналы: chan struct{}, где важен только факт события.
  • Группировка полей внутри конфига: DB struct{ Host string; Port int } прямо в структуре Config.
cfg := struct {
    Host string
    Port int
}{"localhost", 8080}

var payload struct {
    Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { ... }

Анонимные функции (литералы функций)

  • Замыкания захватывают переменные окружения по ссылке на переменную.
  • defer с логикой: defer func(){ ... }().
  • Горутины: go func(){ ... }().
  • Функциональные опции: func WithTimeout(d time.Duration) Option { return func(c *Client){ c.timeout = d } }.
  • Middleware: обёртка, возвращающая http.Handler.
  • Немедленный вызов ограничивает область видимости переменных.
// функциональные опции: на собесах по архитектуре их спрашивают чаще всего
type Option func(*Server)

func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithLogger(l *slog.Logger) Option   { return func(s *Server) { s.log = l } }

func New(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 5 * time.Second}
    for _, o := range opts { o(s) }
    return s
}
Ограничения, о которых спросят
  • У анонимной структуры нельзя объявить методы, только поля.
  • Две анонимные структуры с одинаковым набором полей и тегов - один и тот же тип, их можно присваивать друг другу.
  • Анонимные структуры в сигнатурах публичных функций - плохая идея: вызывающему негде записать этот тип.

Что стоит запомнить

  • Тип определяет набор операций и правила совместимости, а не только размер.
  • Присваивание копирует значение; указатели и ссылочные типы могут сохранить общий объект.
  • Конверсия чисел может терять диапазон без ошибки.

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

  • Не путай размер типа с размером объекта, на который он ссылается.
  • Signed overflow нельзя использовать как проверку границы.

Самопроверка

Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.

Чтение кода

1. Проследи значения

Что напечатает фрагмент? Объясни, почему получился такой результат.

x := int8(127)
x++
fmt.Println(x)
Ответ и разбор

Печатается -128: арифметика int8 остаётся в пределах 8 бит, увеличение максимального значения переходит к минимальному.

Код-ревью

2. Найди ошибку и исправь

Значение count вышло за диапазон int8. Как безопасно преобразовать его, сохранив корректность?

var count int64 = 300
var small int8 = int8(count)
fmt.Println(small)
Ответ и разбор

Преобразование в Go не сообщает об усечении: здесь small станет 44. Сначала проверь диапазон, затем конвертируй: if count < math.MinInt8 || count > math.MaxInt8 { return err }; small := int8(count). Для API лучше вернуть ошибку переполнения, чем принять изменённое значение.

Применение

3. Реши задачу

Функция копирует Point со значениями x=2 и y=7 в другую переменную, после чего меняет копию. Какое значение останется у исходной структуры?

Ответ и разбор

Структура присваивается целиком по значению. Изменение копии не затрагивает исходную, поэтому исходные поля останутся x=2 и y=7.

Применение

4. Реши задачу

Счётчик байтов файла хранится как int32, а файл может быть больше 2 GiB (2³¹ байт). Что изменить и какую проверку оставить при конверсии из int64?

Ответ и разбор

Храни размер в int64 на всём пути. При передаче в API, требующий int, проверь допустимый диапазон для целевой платформы до конверсии. Не полагайся на переполнение или на одинаковую ширину int на всех архитектурах.