JavaRush /Курсы /Go SELF /iota — перечисления (enum) и наборы флагов

iota — перечисления (enum) и наборы флагов

Go SELF
8 уровень , 3 лекция
Открыта

1. Основы iota: зачем и как он считает

Если вы только начинаете программировать, кажется, что всё просто: статус «новая задача» — это 0, «в работе» — это 1, «сделано» — это 2. Но проходит неделя, вы добавляете «на паузе», потом кто-то переставляет значения, а потом ещё и вводят «флаги» (галочки-опции), и код превращается в музей «магических чисел». iota — это как аккуратная линейка: помогает нумеровать значения одинаковым способом, чтобы код читался по смыслу, а не как ребус.

Представьте два варианта кода. Первый — «как получится»:

package main

import "fmt"

func main() {
	const (
		StatusNew        = 0
		StatusInProgress = 1
		StatusDone       = 2
	)

	fmt.Println(StatusDone) // 2
}

Он рабочий — пока никто не ошибся руками. Второй вариант — «как договорились»:

package main

import "fmt"

func main() {
	const (
		StatusNew = iota			// 0
		StatusInProgress			// 1
		StatusDone						// 2
	)

	fmt.Println(StatusDone) // 2
}

Подробности, как это работает, будут чуть ниже. Но смысл тот же, только теперь «нумерация» не размазана по коду: вы ясно видите, что это последовательность.

И да: иногда программисты спорят, что страшнее — магические числа или магические строки. Практический ответ обычно такой: страшнее — магические числа, которые ещё и выглядят очень логично.

Как работает iota: счётчик строк в const-блоке

С iota лучше подружиться как с простым автоматом: у него есть одно правило, и оно не пытается быть умнее вас. iota существует только внутри const ( ... ), стартует с 0 на первой строке блока и увеличивается на 1 на каждой следующей строке.

Не «на каждой объявленной константе», не «на каждой запятой», а именно на строках (логических строках объявления внутри блока). Это важно, потому что вы будете иногда пропускать значения, и тогда нужно понимать, что именно пропустилось.

Вот маленькая табличка, как компилятор «видит» такой блок:

Строка в const-блоке Запись Значение
1
A = iota
0
2
B = iota
1
3
C = iota
2

Проверим это кодом (и заодно потренируемся печатать значения):

package main

import "fmt"

func main() {
	const (
		A = iota
		B = iota
		C = iota
	)

	fmt.Println(A, B, C) // 0 1 2
}

Теперь — важный трюк, без которого iota выглядит скучнее, чем должен. В const-блоке можно не писать выражение справа, и тогда Go повторит выражение с предыдущей строки, просто подставив новое значение iota. То есть компилятор как бы говорит: «Окей, ты не написал формулу — я повторю ту же».

package main

import "fmt"

func main() {
	const (
		A = iota
		B
		C
	)

	fmt.Println(A, B, C) // 0 1 2
}

Ещё один нюанс: iota сбрасывается на 0 в каждом новом const-блоке. Это удобно: вы можете иметь отдельные независимые нумерации (статусы, уровни, коды команд) и не бояться, что они «продолжат счёт» друг друга.

2. Enum и флаги: два разных смысла

Перечисления: одно значение из набора

Перечисление в Go чаще всего делают так: создают смысловой числовой тип (type Status int) и набор констант этого типа. Получается приятный эффект: у вас остаётся число (его легко хранить и сравнивать), но код читает это число как «понятное имя».

А ещё компилятор не даёт случайно смешать «статус» и «например, возраст», потому что это разные типы.

Сделаем маленький enum для статуса задачи в учебном мини-приложении:

package main

import "fmt"

type TaskStatus int

const (
	StatusNew TaskStatus = iota
	StatusInProgress
	StatusDone
)

func main() {
	var s TaskStatus = StatusInProgress
	fmt.Println(s) // 1
}

Тут сразу две важные вещи. Во-первых, TaskStatus — это новый тип, он не равен int. Во-вторых, константы мы явно типизировали как TaskStatus, и это делает проверки читабельными: s == StatusDone выглядит как нормальная фраза.

Теперь добавим человекочитаемый текст через switch:

package main

import "fmt"

type TaskStatus int

const (
	StatusNew TaskStatus = iota
	StatusInProgress
	StatusDone
)

func main() {
	var s TaskStatus = StatusDone

	switch s {
	case StatusNew:
		fmt.Println("status: new") // status: new
	case StatusInProgress:
		fmt.Println("status: in progress")
	case StatusDone:
		fmt.Println("status: done") // status: done
	default:
		fmt.Println("status: unknown")
	}
}

Обратите внимание на default: даже если вы «уверены, что такого значения быть не может», жизнь любит доказывать обратное. Иногда это будет неправильный ввод пользователя, иногда — старые данные, иногда — просто вы забыли обновить одно место в коде. default — это подушка безопасности: не самая мягкая, но лучше, чем падение лицом в runtime.

Наборы флагов: несколько независимых признаков

Enum отвечает на вопрос «какой один вариант выбран?»: статус задачи не может быть одновременно new и done. Но иногда нужно хранить набор независимых признаков.

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

Для этого часто используют битовые флаги: одно число, в котором каждый бит — отдельная «галочка». Сегодня наша цель — понять, как iota помогает создавать такие флаги аккуратно.

Самая частая заготовка выглядит так: 1 << iota. Она означает «возьми число 1 и сдвинь единичный бит на iota позиций». В результате вы получаете степени двойки: 1, 2, 4, 8, 16… — ровно то, что нужно для флагов.

package main

import "fmt"

type TaskFlag uint

const (
	FlagUrgent TaskFlag = 1 << iota
	FlagNotify
	FlagArchived
)

func main() {
	fmt.Println(FlagUrgent, FlagNotify, FlagArchived) // 1 2 4
}

Почему uint, а не int? Потому что флаги логически «маска битов», а не «число, которое бывает отрицательным». На uint проще смотреть, когда вы начнёте выводить двоичное представление и проверять биты: меньше сюрпризов, больше спокойствия.

Enum и flags: как не перепутать

Выглядят они похожими: и там константы, и тут константы, и там iota, и тут iota. Но смысл принципиально разный.

У enum значения идут подряд: 0, 1, 2, 3… и сравниваются через ==. У flags значения должны быть степенями двойки: 1, 2, 4, 8… чтобы их можно было комбинировать в одном числе без «конфликтов».

Посмотрите на эту таблицу — это быстрый способ проверить, вы «в режиме enum» или «в режиме flags»:

Паттерн Как выглядят значения Что означает переменная
Enum (
StatusNew = iota
)
0, 1, 2, 3 ровно одно состояние
Flags (
FlagX = 1 << iota
)
1, 2, 4, 8 набор признаков

Самая типичная ошибка новичка — сделать «флаги» как enum:

// ПЛОХО для флагов: 1, 2, 3 — это не степени двойки
const (
	FlagUrgent = iota + 1 // 1
	FlagNotify            // 2
	FlagArchived          // 3 (вот тут начинается боль)
)

Почему это боль? Потому что 3 в двоичном виде — это 11, то есть «включены сразу два флага». Получается, что один из ваших «флагов» на самом деле означает сразу два. Это примерно как сделать кнопку «Включить свет» и неожиданно включать ещё и микроволновку.

3. Мини‑пример: читаем статус и флаги из ввода

Сейчас мы соберём всё вместе в очень маленький «кусочек приложения»: программа читает из stdin два числа. Первое — код статуса задачи (0/1/2), второе — код маски флагов (например, 3 означает «срочно + уведомлять», если флаги 1 и 2).

Мы специально держим это простым: без функций, структур и коллекций, чтобы фокус был на iota и константах.

package main

import "fmt"

type TaskStatus int
type TaskFlag uint

const (
	StatusNew TaskStatus = iota
	StatusInProgress
	StatusDone
)

const (
	FlagUrgent TaskFlag = 1 << iota
	FlagNotify
	FlagArchived
)

func main() {
	var statusCode int
	var flagsCode uint

	fmt.Scan(&statusCode, &flagsCode)

	status := TaskStatus(statusCode)
	flags := TaskFlag(flagsCode)

	switch status {
	case StatusNew:
		fmt.Println("status: new")
	case StatusInProgress:
		fmt.Println("status: in progress")
	case StatusDone:
		fmt.Println("status: done")
	default:
		fmt.Println("status: unknown")
	}

	// Пока просто запоминаем шаблон проверки флага:
	// "если нужный бит включён, результат & будет не 0"
	if flags&FlagUrgent != 0 {
		fmt.Println("flag: urgent")
	}
	if flags&FlagNotify != 0 {
		fmt.Println("flag: notify")
	}
	if flags&FlagArchived != 0 {
		fmt.Println("flag: archived")
	}
}

Здесь есть важная мысль: iota помог нам сгенерировать правильные константы, а сама работа с масками (операторы &, |, снятие флагов) — это уже «вторая часть истории», где мы будем разбирать битовые операции более системно.

Но базовый шаблон проверки флага можно запомнить уже сейчас: flags&SomeFlag != 0.

4. Приёмы с iota: пропуски и старт не с нуля

Иногда вам хочется, чтобы 0 означал «неизвестно» или «значение не задано», а реальные значения начинались с 1. Это нормальная идея, особенно когда данные приходят извне: 0 часто используют как «пустое значение по умолчанию».

С iota это делается аккуратно: либо вы задаёте первый элемент явно, либо пропускаете первую строку через _.

Вот вариант с явным первым значением:

package main

import "fmt"

type Level int

const (
	LevelUnknown Level = 0
	LevelLow     Level = iota // iota здесь будет 1, потому что это вторая строка
	LevelHigh                 // 2
)

func main() {
	fmt.Println(LevelUnknown, LevelLow, LevelHigh) // 0 1 2
}

А вот вариант «пропустить значение» через _:

package main

import "fmt"

const (
	_ = iota // пропустили 0
	One      // 1
	Two      // 2
)

func main() {
	fmt.Println(One, Two) // 1 2
}

Почему _ полезен? Потому что это честный сигнал читающему: «да, тут намеренно пропуск, это часть дизайна». Гораздо лучше, чем загадочное «а почему вдруг нумерация с 1, и где 0?».

5. Типичные ошибки при работе с iota

Ошибка №1: ожидать, что iota работает вне const-блока.
iota — это не переменная и не функция. Это специальный идентификатор компилятора, который существует только внутри const ( ... ). Попытка использовать iota в var или в обычном выражении в main закончится ошибкой компиляции. Если вам нужен счётчик во время выполнения — это уже обычная переменная и цикл.

Ошибка №2: думать, что iota продолжает считать между разными const-блоками.
Каждый const-блок — отдельная маленькая вселенная, и в ней iota начинается с 0. Это удобно, но если вы ожидали «сквозную нумерацию» по всему файлу, получите неожиданно одинаковые значения в разных группах констант. Когда нужна сквозная нумерация, чаще всего лучше переосмыслить дизайн: обычно группы должны быть независимыми.

Ошибка №3: переставить строки местами и случайно поменять значения.
iota делает нумерацию «по порядку». Это прекрасно, пока порядок действительно важен, и ужасно, если порядок случайный. Если вы добавили значение «в середину», то все последующие сдвинутся. Для enum это иногда допустимо, но для внешних протоколов (например, если числа уже сохранены где-то) — опасно. В таких случаях значения лучше фиксировать явно, а iota использовать осторожно.

Ошибка №4: сделать флаги через iota, но забыть про 1 << iota.
Для флагов недостаточно «0, 1, 2, 3». Нужны степени двойки, иначе комбинации начнут пересекаться. Если вы видите, что «флаги» объявлены как FlagX = iota, это почти всегда красный флажок: правильно будет FlagX = 1 << iota.

Ошибка №5: смешивать enum и flags в одной переменной и проверять их одинаково.
Enum проверяют через ==, потому что там «одно значение из набора». Flags проверяют через маску, потому что там «набор признаков». Когда вы ловите себя на мысли «а почему бы не сделать статус тоже флагами?» — остановитесь и задайте вопрос: «задача может быть одновременно done и in progress?». Если нет — это enum. Если да — возможно, это уже другой домен, и вам нужно разделить понятия.

1
Задача
Go SELF, 8 уровень, 3 лекция
Недоступна
Городской светофор
Городской светофор
1
Задача
Go SELF, 8 уровень, 3 лекция
Недоступна
Два счётчика
Два счётчика
1
Задача
Go SELF, 8 уровень, 3 лекция
Недоступна
Статус посылки
Статус посылки
1
Задача
Go SELF, 8 уровень, 3 лекция
Недоступна
Флаги уведомлений
Флаги уведомлений
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Дмитрий Уровень 14
1 мая 2026
Если нужен старт начального значения не с нуля,можно обойтись без _. Использовать выражения с iota,например: LevelLow Level = iota + 1
Vlad Tagunkov Уровень 24
19 апреля 2026
про флаги не совсем понятно. надеюсь потом в задачах это проясниться зачем нужны эти флаги.
Igor Kh Уровень 12
26 апреля 2026
это фактически число в двоичной системе, например 10001001 (128 64 32 16 8 4 2 1) единицы в этой последовательности - включенные биты, то есть тут включены 128 8 1 , что можно представить в виде десятичного числа 128 + 8 + 1 = 137 ( читай перевод из двоичной в десятичную систему), и затем проверить включен ли этот бит через 137 & 8 == 8 (т.е. не 0), тоже самое с 128 & 1 == 1, а вот например 137 & 2 == 0 так, как второй бит не включен