1. Введение
Когда программа маленькая, кажется, что написать if status == 2 — это нормально. Но проходит неделя (или один созвон с тимлидом), и вы уже не помните: «2 — это done или in progress?». А если таких «2» по проекту двадцать, то начинается игра «угадай смысл по лицу автора кода». Enum и flags — это простая техника, которая превращает числа в понятные имена и резко снижает шанс ошибок.
Представим, что мы делаем мини‑консольную программу TaskInspector. Она пока умеет очень мало: мы вводим статус задачи и некоторые «флажки» (например, важная задача или с дедлайном), а программа печатает понятное описание. Сегодня мы не делаем архитектуру, не делаем файлы, не делаем коллекции — мы просто учимся писать и читать код, где числа не выглядят как шифрограмма.
Enum vs flags: это разные модели
Enum и flags похожи тем, что и там, и там используются константы, и часто даже iota. Но смысл у них принципиально разный:
- Enum — это «выбери ровно один вариант из набора».
- Flags — это «выбери любое множество признаков одновременно».
Если перепутать эти модели, код может компилироваться и даже «иногда работать», но читать его будет больно всем — включая вас через месяц.
Короткая таблица, чтобы зафиксировать различие:
| Модель | Вопрос, на который отвечает | Как выглядит проверка | Пример |
|---|---|---|---|
| Enum | «Какое одно состояние сейчас?» | == (равенство одному значению) | статус задачи: new / in_progress / done |
| Flags | «Какие признаки включены?» | & с маской и сравнение с 0 | задача важная? с дедлайном? заблокирована? |
2. Enum‑паттерн в Go
Свой тип + константы + понятные проверки
Когда вы пишете type Status int, вы создаёте новый тип, а не «переименованный int». Это удобно: компилятор не даст вам случайно сравнить статус задачи с чем-то посторонним (например, с количеством попыток или с ценой шаурмы).
Минимальный скелет enum‑типа статуса:
package main
import "fmt"
func main() {
type Status int
const (
StatusNew Status = iota
StatusInProgress
StatusDone
)
var s Status = StatusDone
fmt.Println(s == StatusDone) // true
}
Обратите внимание: мы сравниваем s == StatusDone, а не s == 2. Условие читается как «статус равен Done», а не «статус равен 2 (почему 2?)».
Человекочитаемый вывод: «число внутри, слово снаружи»
Enum почти всегда хранится числом — это нормально и даже полезно (компактно). Но человеку нужен текст. Поэтому рядом с enum обычно появляется маленький «переводчик»: логика, которая превращает значение enum в строку.
Если печатать статус так: fmt.Println(s), вы получите 2. Формально корректно, но «человеко‑бесполезно». В учебных примерах (и во многих реальных) проще всего сделать сопоставление через switch:
package main
import "fmt"
func main() {
type Status int
const (
StatusNew Status = iota
StatusInProgress
StatusDone
)
s := StatusInProgress
text := "unknown"
switch s {
case StatusNew:
text = "new"
case StatusInProgress:
text = "in_progress"
case StatusDone:
text = "done"
}
fmt.Println(text) // in_progress
}
Здесь важно, что у нас есть «план Б» — "unknown". В реальных программах данные могут быть кривыми (особенно если они приходят извне), и «unknown» — это не признак слабости, а признак того, что вы не верите миру на слово (и правильно делаете).
3. Знакомство с фунциями
Вы уже видели, что в main легко накопить много логики: switch, куча if, сборка строк… И очень быстро main превращается в «простыню», в которой сложно найти смысл. Функции — это способ вынести кусок логики в отдельный блок с именем.
Что такое функция простыми словами
Функция — это «мини-программа внутри программы»:
- у неё есть вход (параметры),
- есть выход (то, что она возвращает),
- и есть тело (код, который выполняется).
Например, мы хотим превратить статус в текст. Это можно написать прямо в main, но лучше назвать правило отдельно: statusText.
Как читать объявление функции
Посмотрите на эту строку:
func statusText(s Status) string
Она читается так:
- func — объявляем функцию;
- statusText — имя функции;
- (s Status) — вход: один параметр s типа Status;
- string после скобок — выход: функция возвращает строку.
То есть: «Функция statusText принимает Status и возвращает string».
Return: «вернуть результат и закончить функцию»
Оператор return делает две вещи одновременно:
- возвращает значение вызывающему коду;
- заканчивает выполнение функции (дальше код в этой функции не идёт).
Вы уже встречали return как «выйти из main раньше». В функциях смысл тот же, просто теперь мы обычно возвращаем результат.
Например, в switch удобно возвращать строку прямо из нужной ветки:
func statusText(s Status) string {
switch s {
case StatusNew:
return "new"
case StatusInProgress:
return "in_progress"
case StatusDone:
return "done"
default:
return "unknown"
}
}
Обратите внимание: здесь нет «общей переменной text», которую мы меняем. Вместо этого каждая ветка сразу возвращает ответ. Это один из самых читаемых стилей в Go: меньше состояния, меньше шансов ошибиться.
Как вызывают функцию (и куда девается результат)
Вызов выглядит как «имя + скобки»:
t := statusText(StatusDone)
fmt.Println(t)
Мы вызываем statusText(...), получаем строку, а затем передаём её в fmt.Println. Можно и компактнее:
fmt.Println(statusText(StatusDone))
Функции‑помощники
Вот как будет выглядеть ваша программа, когда вы вынесите преобразования статуса в текст в отдельную функцию statusText. Даже если вы пока не чувствуете себя уверенно с функциями, этот код можно воспринимать как «чёрный ящик: положили статус — получили строку».
package main
import "fmt"
type Status int
const (
StatusNew Status = iota
StatusInProgress
StatusDone
)
func statusText(s Status) string {
switch s {
case StatusNew:
return "new"
case StatusInProgress:
return "in_progress"
case StatusDone:
return "done"
default:
return "unknown"
}
}
func main() {
fmt.Println(statusText(StatusDone)) // done
}
Такие «переводчики» встречаются постоянно: «код → текст», «флаг → да/нет», «тип → имя». Их ценность в том, что они централизуют правило: если вы решили, что StatusInProgress печатается как "in-progress", вы меняете это в одном месте.
Небольшая ремарка: комментарии с ожидаемым выводом — полезная привычка для обучения и самопроверки.
4. Flags‑паттерн в Go
Набор признаков в одном числе: каждый флаг — отдельный бит
Для flags ключевая идея такая: каждый флаг — это отдельный бит. Поэтому флаги почти всегда задаются как 1 << iota, а не просто iota.
Если вы сделаете флаги 0, 1, 2, 3, то это будет enum, а не flags (и дальше вы сами себе устроите логическую ловушку).
Пусть у задачи будут признаки: Urgent, HasDeadline, Blocked. Мы заведём тип TaskFlags на базе uint и определим флаги степенями двойки:
package main
import "fmt"
type TaskFlags uint
const (
FlagUrgent TaskFlags = 1 << iota
FlagHasDeadline
FlagBlocked
)
func main() {
flags := FlagUrgent | FlagBlocked
fmt.Println(flags&FlagHasDeadline != 0) // false
}
Здесь важно два момента:
- включение флагов делается через | (OR): это «добавить признак»;
- проверка делается через & (AND) и сравнение с нулём: это «есть ли пересечение с маской».
Если попробовать проверить флаг так: flags == FlagUrgent, вы сломаете смысл flags. Потому что flags может содержать несколько флагов, и равенство одному флагу будет истинно только в частном случае (когда включён ровно один флаг).
Helper‑функции для flags: читаемость важнее «я и так помню &»
Битовые операции — штука честная, но визуально «колючая»: flags = flags &^ FlagBlocked выглядит как заклинание. Чтобы код читался как текст, часто делают маленькие helper‑функции: hasFlag, addFlag, removeFlag.
Это не «ускорение программы» (компилятор и так быстр), это ускорение чтения кода человеком.
Минимальный helper для проверки:
package main
import "fmt"
type TaskFlags uint
const (
FlagUrgent TaskFlags = 1 << iota
FlagHasDeadline
FlagBlocked
)
func hasFlag(all, f TaskFlags) bool { return all&f != 0 }
func main() {
flags := FlagUrgent | FlagBlocked
fmt.Println(hasFlag(flags, FlagBlocked)) // true
}
Теперь if hasFlag(flags, FlagBlocked) читается нормально даже у того, кто не держит в голове побитовые операторы (например, у вас в понедельник утром).
Человекочитаемый вывод flags: не «13», а «urgent|blocked»
Когда вы выводите flags как число, вы получаете что-то вроде 5 или 13. Это честно, но не объясняет, какие именно признаки включены. Поэтому рядом с flags часто появляется «сборщик строки»: он превращает набор флагов в понятный ярлык.
Мы не будем делать авто‑генерацию и «умную магию», а соберём строку простыми if. Да, немного ручной работы, зато максимально прозрачно.
package main
import "fmt"
type TaskFlags uint
const (
FlagUrgent TaskFlags = 1 << iota
FlagHasDeadline
FlagBlocked
)
func flagsText(f TaskFlags) string {
out := ""
if f&FlagUrgent != 0 {
out += "urgent|"
}
if f&FlagHasDeadline != 0 {
out += "deadline|"
}
if f&FlagBlocked != 0 {
out += "blocked|"
}
if out == "" {
return "none"
}
return out[:len(out)-1] // убираем последний '|'
}
func main() {
fmt.Println(flagsText(FlagUrgent | FlagBlocked)) // urgent|blocked
}
Здесь есть маленькая деталь, которая выглядит чуть «хитро»: out[:len(out)-1]. Мы отрезаем последний разделитель "|". В больших программах можно решать это иначе, но для маленького примера такой способ хорошо демонстрирует идею «собираем строку по частям».
5. Мини‑приложение TaskInspector
Теперь соберём всё в один сценарий. Пользователь вводит два числа: statusCode и flagsCode. Мы превращаем их в Status и TaskFlags, печатаем текст статуса и расшифровку флагов.
Это ещё не «настоящий трекер задач», но уже полезный кирпичик: мы научились не хранить смысл в голове, а хранить его в коде — через константы и функции‑помощники.
package main
import "fmt"
type Status int
type TaskFlags uint
const (
StatusNew Status = iota
StatusInProgress
StatusDone
)
const (
FlagUrgent TaskFlags = 1 << iota
FlagHasDeadline
FlagBlocked
)
func statusText(s Status) string {
switch s {
case StatusNew:
return "new"
case StatusInProgress:
return "in_progress"
case StatusDone:
return "done"
default:
return "unknown"
}
}
func flagsText(f TaskFlags) string {
out := ""
if f&FlagUrgent != 0 {
out += "urgent "
}
if f&FlagHasDeadline != 0 {
out += "deadline "
}
if f&FlagBlocked != 0 {
out += "blocked "
}
if out == "" {
return "none"
}
return out
}
func main() {
var statusCode int
var flagsCode uint
fmt.Scan(&statusCode, &flagsCode)
s := Status(statusCode)
f := TaskFlags(flagsCode)
fmt.Println(statusText(s)) // пример: in_progress
fmt.Println(flagsText(f)) // пример: urgent blocked
}
Если запустить это и ввести, например, 1 5, то 1 будет StatusInProgress, а 5 в двоичном виде 101, то есть это FlagUrgent (бит 0) + FlagBlocked (бит 2). И программа распечатает «in_progress» и «urgent blocked».
6. Типичные ошибки
Ошибка №1: путать enum и flags и проверять flags через ==.
Это одна из самых частых проблем: программист видит «именованные константы» и начинает сравнивать всё через равенство. Но flags — это набор, и проверка должна идти через маску: flags&FlagX != 0. Равенство уместно только для enum, где значение по смыслу одно.
Ошибка №2: задавать флаги как iota вместо 1 << iota.
Если вы напишете FlagRead = iota, вы получите значения 0, 1, 2, 3…, и это перестанет быть битовой маской. Такое «почти работает», пока вы не начнёте комбинировать признаки, а потом внезапно выясняется, что «3» значит не то, что вы думали.
Ошибка №3: печатать enum/flags «как число» и удивляться, что никто ничего не понимает.
Компьютер понимает, что 2 — это done, но пользователю, тестировщику и вам самим через неделю нужен текст. Для enum делайте switch в строку, для flags — разбор набора флагов в ярлык. Это не украшательство, это практическая диагностика.
Ошибка №4: не иметь ветку default у enum‑переводчика.
Даже если «у нас всегда корректные данные», реальность любит сюрпризы. Если вы не предусмотрели неизвестное значение, у вас либо будет пустая строка, либо странный вывод, либо некорректная логика. default: return "unknown" — простая страховка.
Ошибка №5: писать побитовую логику «в лоб» по всему коду и не использовать helper‑функции там, где они улучшают чтение.
Битовые выражения сами по себе нормальные, но когда они повторяются, код становится похож на математическую контрольную. Маленькие функции‑помощники вроде hasFlag и «переводчики» в строку не добавляют магии — они добавляют смысловые имена, а значит уменьшают шанс ошибиться.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ