Идиоматический способ представления перечислений (enum) в Go
Идиоматический подход: тип + iota + методы
В Go нет встроенного типа enum, но сообщество выработало устойчивый паттерн, который считается идиоматическим. Он основан на комбинации: пользовательский тип на базе int (или другого числового типа), константы с iota и методы для валидации/строкового представления. Этот подход даёт типобезопасность и гибкость.
Базовый пример: статус заказа
package main
import "fmt"
// OrderStatus — пользовательский тип для статуса заказа
type OrderStatus int
// Константы-значения перечисления
const (
StatusNew OrderStatus = iota // 0
StatusProcessing // 1
StatusShipped // 2
StatusDelivered // 3
StatusCancelled // 4
)
// String — реализация интерфейса Stringer для красивого вывода
func (s OrderStatus) String() string {
switch s {
case StatusNew:
return "New"
case StatusProcessing:
return "Processing"
case StatusShipped:
return "Shipped"
case StatusDelivered:
return "Delivered"
case StatusCancelled:
return "Cancelled"
default:
return fmt.Sprintf("OrderStatus(%d)", int(s))
}
}
// IsValid — проверка, что значение принадлежит перечислению
func (s OrderStatus) IsValid() bool {
switch s {
case StatusNew, StatusProcessing, StatusShipped, StatusDelivered, StatusCancelled:
return true
default:
return false
}
}
func main() {
status := StatusNew
fmt.Println(status) // Вывод: New
// Проверка на допустимость
invalidStatus := OrderStatus(99)
fmt.Println(invalidStatus.IsValid()) // false
fmt.Println(invalidStatus) // OrderStatus(99)
}
Почему это идиоматично?
- Типобезопасность: Компилятор не позволит передать
intтуда, где ожидаетсяOrderStatus. - Компактность:
iotaавтоматически присваивает значения, начиная с 0. Если нужно начать с другого числа — используйтеiota + 1. - Интерфейс
Stringer: МетодString()автоматически вызывается при использовании%vиfmt.Println. - Валидация: Метод
IsValid()— стандартный способ проверки, что значение не выходит за пределы определённых констант.
Продвинутый вариант: с парсингом из строки
Если нужно конвертировать строку обратно в enum, добавьте функцию-парсер:
// ParseOrderStatus — преобразует строку в OrderStatus (регистронезависимо)
func ParseOrderStatus(s string) (OrderStatus, error) {
switch strings.ToLower(s) {
case "new":
return StatusNew, nil
case "processing":
return StatusProcessing, nil
case "shipped":
return StatusShipped, nil
case "delivered":
return StatusDelivered, nil
case "cancelled":
return StatusCancelled, nil
default:
return 0, fmt.Errorf("invalid order status: %s", s)
}
}
Когда использовать другие подходы?
- Строковые константы — если важен читаемый JSON/сериализация (но теряется типобезопасность).
- Пакет
iotaс битовыми масками — для флагов (например, права доступа). - Сторонние генераторы (stringer, enum) — для больших проектов с десятками enum.
Для 90% случаев описанный выше паттерн — это то, что нужно. Он прост, понятен и соответствует философии Go: «явное лучше неявного».
<<Альтернатива: строковые константы с пользовательским типом
Иногда числовые значения неудобны — например, при сериализации в JSON хочется видеть строки. В таких случаях можно определить тип на основе string:
type OrderStatus string
const (
StatusNew OrderStatus = "new"
StatusProcessing OrderStatus = "processing"
StatusShipped OrderStatus = "shipped"
StatusDelivered OrderStatus = "delivered"
StatusCancelled OrderStatus = "cancelled"
)
func (s OrderStatus) IsValid() bool {
switch s {
case StatusNew, StatusProcessing, StatusShipped, StatusDelivered, StatusCancelled:
return true
default:
return false
}
}
Плюсы:
- JSON-сериализация сразу даёт читаемые строки без дополнительных тегов.
- Не нужно писать
String()— строка и так является значением. - Удобно для логирования и отладки.
Минусы:
- Меньшая производительность при сравнении (хотя для большинства приложений это не критично).
- Легче ошибиться при передаче произвольной строки (но метод
IsValid()решает эту проблему). - Нельзя использовать в switch как числовые значения (но можно сравнивать строки).
Выбор между числовым и строковым enum зависит от контекста. Для внутренней логики чаще используют числа, для API/JSON — строки. Оба подхода идиоматичны и широко применяются в Go-проектах.