Ключевого слова enum в Go нет. Перечисление собирается из двух частей: именованного типа и блока const со значениями этого типа, пронумерованными через iota.
Sunday равен 0, а каждая следующая строка на единицу больше. Именно именованный тип Weekday делает это перечислением, а не списком чисел: isWeekend в своей сигнатуре сообщает, что ожидает, а к типу можно добавить методы. Вывод печатает 1 5 6, потому что пока ничто не говорит Go, как показывать Weekday текстом. Об этом ниже.
Как работает iota
iota это счётчик, который компилятор предоставляет внутри блока const. Два правила объясняют все приёмы на его основе:
iotaравен номеру текущей строки в блоке, начиная с 0, и сбрасывается в 0 в каждом новом блокеconst.- Строка без
= выражениеповторяет выражение и тип предыдущей строки, вычисленные с новымiota.
Поэтому Monday выше это сокращение для Monday Weekday = iota, где iota уже равен 1. Раз выражение повторяется, оно может быть любым константным выражением, а не только iota:
iota считает строки, а не имена: две константы в одной строке делят одно значение iota, как показывают X и Y.
Начать с 1, и почему этого можно не делать
Переменная типа-перечисления, которой никто не присвоил значение, хранит 0, своё нулевое значение. Если 0 это настоящее значение вроде Sunday, нельзя отличить «пользователь выбрал воскресенье» от «поле так и не заполнили». Три распространённых решения:
Вариант 1 самый частый в продакшен-коде, и перечисления Go, сгенерированные из protobuf, следуют ему (..._UNSPECIFIED = 0). Нулевое значение тогда означает что-то честное.
Пропуск значений
Пустой идентификатор _ расходует значение iota, не создавая имени. С его помощью оставляют пропуски, например чтобы совпасть с числами из протокола или убрать значение, не перенумеровывая остальные:
type Opcode byte
const (
OpContinue Opcode = iota // 0
OpText // 1
OpBinary // 2
_ // 3, reserved
_ // 4, reserved
_ // 5, reserved
_ // 6, reserved
_ // 7, reserved
OpClose // 8
OpPing // 9
OpPong // 10
)
Когда числа зафиксированы внешней спецификацией, как у этих опкодов WebSocket, явная запись (OpClose Opcode = 8) часто понятнее, чем подсчёт пропусков. iota нужен для значений, конкретные номера которых вам безразличны.
Никогда не переставляйте и не вставляйте строки в список iota, если его числа сохраняются в базе данных, файле или отправляются по сети. Добавленная в середину строка сдвигает все значения после неё. Новые значения добавляйте в конец или задавайте числа явно.
Добавляем метод String
Дайте типу метод String() string, и fmt будет использовать его для %v, %s и Println:
В этом методе важны две детали:
- Проверка границ. Без неё
Weekday(9).String()запаникует с выходом индекса за границы, и рано или поздно это случится, потому что ничто не мешает вызывающему коду создатьWeekday(9). int(d)внутриSprintf. Форматировать самdчерез%dможно, но форматирование через%vснова вызоветString(), и рекурсия продолжится до переполнения стека.
%d по-прежнему печатает число, так что доступны обе формы: Wednesday is day 3.
Генерация String через stringer
Для длинных списков метод за вас напишет утилита stringer:
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...
Она создаёт weekday_string.go с компактной реализацией String(), а также проверку на этапе компиляции, которая ломает сборку, если константы изменились, а код не перегенерировали. Флаг -linecomment берёт имя из комментария в конце строки, что удобно для имён с пробелами.
Проверка значений
Перечисление в Go не замкнуто. Любое значение базового типа преобразуется в него, а нетипизированные константы преобразуются неявно:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
Поэтому проверяйте значения, пришедшие извне вашего кода (JSON, базы данных, флаги, другие пакеты):
Неэкспортированный маркер colorCount в конце блока сохраняет корректность IsValid при добавлении новых цветов, потому что он всегда стоит на единицу дальше последнего настоящего значения.
switch по перечислению
Перечисления обычно обрабатываются через switch. Go не проверяет, что switch покрывает все значения, поэтому добавьте default, который сообщает о неожиданности:
func (c Color) Hex() string {
switch c {
case Red:
return "#ff0000"
case Green:
return "#00ff00"
case Blue:
return "#0000ff"
default:
return "#000000"
}
}
Сторонний линтер exhaustive (входит в golangci-lint) сообщает о switch по типам-перечислениям, где пропущен вариант, и это даёт большую часть того, что в других языках дают проверки полноты перечислений.
Перечисления битовых флагов
Когда значения комбинируются, как права доступа, используйте по одному биту на значение через 1 << iota:
| объединяет флаги, & проверяет их, а &^ (оператор AND NOT в Go) сбрасывает. Здесь правильно выбрать беззнаковый базовый тип: uint8 вмещает 8 флагов, uint64 вмещает 64.
Строковые перечисления
Когда значение всё равно хранится или передаётся текстом, тип на основе строки избавляет от слоя преобразований:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
Значения печатаются и сериализуются читаемо без метода String(), а столбец базы данных хранит "prod", а не число, зависящее от порядка объявления. Компромиссы: сравнения становятся сравнениями строк, битовые флаги невозможны, а проверка всё равно на вас, ведь Env("banana") тоже компилируется.
Перечисления и JSON
Целочисленное перечисление по умолчанию сериализуется как число. Чтобы читать и писать имена, реализуйте encoding.TextMarshaler и encoding.TextUnmarshaler. encoding/json использует их для значений и для ключей мап:
У MarshalText получатель-значение, поэтому он работает и для Level, и для *Level; UnmarshalText нужен получатель-указатель, потому что он меняет значение. Эти же два метода позволяют типу работать с TextVar из пакета flag и с большинством библиотек конфигурации.
Подводные камни
- Неявное преобразование литералов. Функция, принимающая
Weekday, примет и нетипизированную константу42. Отвергаются только типизированные значения другого типа. - Тип не указан в первой строке. В
const ( Red = iota; Green; Blue )все три это нетипизированные целочисленные константы, а не значенияColor, так что методыColorк ним не относятся. ПишитеRed Color = iota, чтобы повторяемое выражение несло тип. - Перестановка сохраняемых перечислений. Вставка значения в середину блока
iotaмолча меняет числа, которые уже где-то сохранены. - Рекурсия в String. Внутри
String()никогда не форматируйте получатель через%vили%s. Сначала преобразуйте его в базовый тип.
Часто задаваемые вопросы
Есть ли в Go перечисления (enum)?
Как возможности языка нет. Ключевого слова enum не существует. Идиоматичная замена: именованный тип плюс блок типизированных констант, обычно пронумерованных через iota:
type Color int
const (
Red Color = iota
Green
Blue
)
Тип даёт читаемые сигнатуры и место для методов вроде String(). Он не мешает написать Color(42), поэтому значения, пришедшие извне, нужно проверять.
Что такое iota в Go?
iota это предопределённый идентификатор, равный номеру текущей строки (спецификации константы) внутри блока const, начиная с 0. В каждом новом блоке const он сбрасывается в 0. Если в строке нет выражения, Go повторяет предыдущее выражение со следующим значением iota, поэтому Red = iota; Green; Blue даёт 0, 1, 2.
Как сделать, чтобы iota начинался с 1?
Напишите в первой строке First Kind = iota + 1 или пропустите ноль через пустой идентификатор: _ = iota, затем First. Многие Go-разработчики вместо этого оставляют 0 и называют его Unknown или Invalid, чтобы неинициализированная переменная (нулевое значение которой 0) явно не была настоящим вариантом.
Как вывести enum строкой в Go?
Дайте типу метод String() string. fmt вызывает его для %v, %s и Println, поэтому fmt.Println(Green) печатает Green, а не 1. Метод можно написать вручную через switch или массив либо сгенерировать командой go run golang.org/x/tools/cmd/stringer@latest -type=Color.
Как преобразовать строку в enum в Go?
Напишите функцию разбора, которая ищет строку, обычно в map[string]Color или через switch, и возвращает ошибку для неизвестного ввода: func ParseColor(s string) (Color, error). Если реализовать UnmarshalText с той же логикой, JSON, флаги и загрузчики конфигурации будут использовать её автоматически.