Как правильно и идиоматично реализовать перечисления (enums) в Go?
Идиоматичный способ создания enum в Go
В Go действительно нет нативного типа enum, как в Java или Python. Это осознанное решение разработчиков языка: они считают, что перечисления в классическом виде усложняют язык и не соответствуют философии Go (простота, явность, минимализм). Вместо этого Go предлагает комбинацию именованных констант с iota и пользовательских типов на основе int.
Базовый подход: тип + iota
Вот как правильно реализовать ваш пример со статусами заказа:
package main
import (
"fmt"
)
// Определяем пользовательский тип на основе int
type OrderStatus int
// Определяем константы с помощью iota
const (
StatusNew OrderStatus = iota // 0
StatusProcessing // 1
StatusShipped // 2
StatusDelivered // 3
StatusCancelled // 4
)
func main() {
var status OrderStatus = StatusShipped
fmt.Println(status) // Выведет просто число: 2
}
Это работает, но есть проблема — вывод в консоль показывает число, а не осмысленное имя. Давайте это исправим.
Добавляем метод String() для красивого вывода
// String возвращает строковое представление статуса
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("Unknown(%d)", int(s))
}
}
func main() {
status := StatusShipped
fmt.Println(status) // Теперь выведет: Shipped
// Для логов и JSON тоже работает
log.Printf("Current status: %s", status)
}
Защита от невалидных значений
Чтобы проверять, что значение входит в допустимый диапазон, добавьте метод IsValid():
// IsValid проверяет, является ли статус допустимым
func (s OrderStatus) IsValid() bool {
switch s {
case StatusNew, StatusProcessing, StatusShipped, StatusDelivered, StatusCancelled:
return true
}
return false
}
// Функция для безопасного создания статуса из числа
func ParseOrderStatus(value int) (OrderStatus, error) {
s := OrderStatus(value)
if !s.IsValid() {
return 0, fmt.Errorf("invalid OrderStatus: %d", value)
}
return s, nil
}
func main() {
// Так нельзя — будет ошибка компиляции
// var s OrderStatus = 999 // ошибка: cannot use 999 (untyped int constant) as OrderStatus
// А так можно — явное приведение типа
s := OrderStatus(999)
fmt.Println(s.IsValid()) // false
// Безопасное создание
validStatus, err := ParseOrderStatus(2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(validStatus) // Shipped
}
}
Полный рабочий пример
package main
import (
"fmt"
"log"
)
type OrderStatus int
const (
StatusNew OrderStatus = iota
StatusProcessing
StatusShipped
StatusDelivered
StatusCancelled
)
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("Unknown(%d)", int(s))
}
}
func (s OrderStatus) IsValid() bool {
switch s {
case StatusNew, StatusProcessing, StatusShipped, StatusDelivered, StatusCancelled:
return true
}
return false
}
func main() {
status := StatusShipped
log.Printf("Order status: %s", status) // Order status: Shipped
// Проверка
fmt.Println("Is valid:", status.IsValid()) // true
// Использование в switch
switch status {
case StatusNew:
fmt.Println("Order is new")
case StatusShipped:
fmt.Println("Order has been shipped")
}
}
Дополнительные рекомендации
- Не используйте iota для значений, которые могут быть записаны в базу данных или переданы по API — если вы добавите новое значение в середину списка, все существующие данные сломаются. В таких случаях задавайте значения явно:
StatusNew OrderStatus = 1. - Для JSON-сериализации реализуйте интерфейсы
json.Marshalerиjson.Unmarshaler, чтобы работать со строками, а не числами. - Для больших enum (20+ значений) используйте маппинг через map или срез для метода String(), чтобы избежать длинного switch.
- Всегда обрабатывайте default-кейс в switch для метода String() — это защита от будущих ошибок.
Такой подход полностью покрывает потребности: строгая типизация, удобный вывод, проверка валидности. И да, это идиоматично для Go — посмотрите на стандартную библиотеку (например, time.Weekday), там используется точно такой же паттерн.
Альтернативный подход: использование map для строковых представлений
Если ваш enum содержит много значений (20+), длинный switch в методе String() становится громоздким. Можно использовать map для более чистого решения:
type OrderStatus int
const (
StatusNew OrderStatus = 1
StatusProcessing OrderStatus = 2
StatusShipped OrderStatus = 3
StatusDelivered OrderStatus = 4
StatusCancelled OrderStatus = 5
)
var statusNames = map[OrderStatus]string{
StatusNew: "New",
StatusProcessing: "Processing",
StatusShipped: "Shipped",
StatusDelivered: "Delivered",
StatusCancelled: "Cancelled",
}
func (s OrderStatus) String() string {
if name, ok := statusNames[s]; ok {
return name
}
return fmt.Sprintf("Unknown(%d)", int(s))
}
func (s OrderStatus) IsValid() bool {
_, ok := statusNames[s]
return ok
}
Плюсы этого подхода:
- Легко добавлять новые значения — достаточно обновить map.
- Меньше шансов ошибиться в switch (забыть case).
- Можно легко добавить обратный map:
map[string]OrderStatusдля парсинга из строки.
Минусы:
- Немного медленнее, чем switch (но для большинства приложений это не критично).
- Map инициализируется при старте программы, что может быть избыточно для простых enum.
Выбирайте подход под задачу: для 3-5 значений — switch, для 10+ — map. Оба варианта идиоматичны.