Avatar ·

Идиоматический способ представления перечислений (enum) в Go

Всем привет! Я новичок в Go, перехожу с Python и Java, и никак не могу найти нормальный способ сделать перечисление (enum). В других языках я привык писать что-то вроде: ```python class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 ``` В Go же нет встроенного типа `enum`. Я попробовал использовать константы с `const` и `iota`, но чувствую, что это неполноценно. Например, я хочу создать тип для статуса заказа: `New`, `Processing`, `Shipped`, `Delivered`, `Cancelled`. Мне нужно, чтобы: 1. Переменная могла принимать только одно из этих значений. 2. Была возможность проверить, является ли значение допустимым. 3. Хорошо бы иметь метод `String()` для красивого вывода. 4. И чтобы это было идиоматично, как принято в Go-сообществе. Пока я сделал так: ```go package main import "fmt" type OrderStatus int const ( New OrderStatus = iota Processing Shipped Delivered Cancelled ) func main() { status := New fmt.Println(status) // выводит 0, а хотелось бы "New" } ``` Но это выводит просто число. Я могу написать функцию `String()`, но не уверен, что это правильный путь. Как в Go принято делать полноценные enum? Есть ли какой-то стандартный паттерн? Заранее спасибо за помощь! <<>>

Идиоматический подход: тип + 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-проектах.