JavaRush /Курсы /Go SELF /Enum и flags в Go: закрепляем на практике

Enum и flags в Go: закрепляем на практике

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

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 делает две вещи одновременно:

  1. возвращает значение вызывающему коду;
  2. заканчивает выполнение функции (дальше код в этой функции не идёт).

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

1
Задача
Go SELF, 8 уровень, 5 лекция
Недоступна
Светофор переводчик
Светофор переводчик
1
Задача
Go SELF, 8 уровень, 5 лекция
Недоступна
Заказ финальность
Заказ финальность
1
Задача
Go SELF, 8 уровень, 5 лекция
Недоступна
Права файла
Права файла
1
Задача
Go SELF, 8 уровень, 5 лекция
Недоступна
Инспектор задач
Инспектор задач
1
Опрос
Константы и битовые операции, 8 уровень, 5 лекция
Недоступен
Константы и битовые операции
Константы, iota и битовые операции
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ