Как использовать перечисления в Golang

Андрей Шагин
NOP::Nuances of Programming
5 min readJul 2, 2024

Введение

В Todo приложении на Go у каждой задачи имеется статус: завершена (completed), архивирована (archived) или удалена (deleted).

Разработчику необходимо убедиться: когда задачи создаются или обновляются через конечные точки API, принимаются только допустимые значения статуса; если же пользователи пытаются задать недопустимый статус, им предоставляются четкие сообщения об ошибках.

С этим справятся перечисления.

В Go нет перечислений, но есть возможность их реализовать.

В этом руководстве и начинающие, и опытные разработчики научатся эффективно управлять перечислениями в Golang, обеспечивая методами проверки целостность и надежность данных, расширят свои возможности задействовать их мощь в проектах на Go.

Что такое «перечисление»?

Это отдельный набор именованных констант. В языках вроде Java и C++ имеется встроенная поддержка перечислений, в Go используется другой подход.

Чтобы сымитировать свойственное перечислениям поведение, вместо специального типа enum разработчики здесь применяют константы или пользовательские типы с iota.

В чем преимущества перечислений?

  • Код с ними удобнее для восприятия: вместо необработанных целочисленных или строковых констант применяются информативные названия значений. Поэтому он становится четче, а кодовая база — понятнее и проще в сопровождении для других разработчиков.
  • С ними выполняются проверки во время компиляции, поэтому ошибки выявляются на ранней стадии разработки, переменным не присваиваются недопустимые или неожиданные значения, снижается вероятность ошибок во время выполнения.
  • С ними возможные значения переменной ограничены предопределенным набором. Так предотвращаются логические ошибки, обусловленные неправильными или неожиданными значениями.

Определение перечислений константами

Перечисления в Go легко определять константами. Вот простой пример определения перечислимых констант для дней недели:

package main

import "fmt"

const (
SUNDAY = "Sunday"
MONDAY = "Monday"
TUESDAY = "Tuesday"
WEDNESDAY = "Wednesday"
THURSDAY = "Thursday"
FRIDAY = "Friday"
SATURDAY = "Saturday"
)

func main() {
day := MONDAY
fmt.Println("Today is ", day) // "Today is Monday" Сегодня понедельник
}

Моделирование перечислений с пользовательскими типами и iota

Константы хороши для простых перечислений, пользовательские типы с iota — более идиоматичный подход в Go.

Что такое iota?

iota — специальный идентификатор Go, которым совместно с объявлениями const автоматически генерируется последовательность связанных значений.

С ним упрощается процесс определения последовательных значений, особенно при определении перечислений или объявлении наборов констант с приращением значений.

В объявлении const этот iota начинается со значения 0 и увеличивается на 1 при каждом следующем появлении в том же блоке const. Когда iota появляется в нескольких объявлениях const того же блока, его значение обнуляется для каждого нового блока const:

package main

import "fmt"

const (
SUNDAY = iota // Воскресенью присваивается 0
MONDAY // Понедельнику присваивается 1, прирост относительно воскресенья
TUESDAY // Вторнику присваивается 2, прирост относительно понедельника
WEDNESDAY // Среде присваивается 3, прирост относительно вторника
THURSDAY // Четвергу присваивается 4, прирост относительно среды
FRIDAY // Пятнице присваивается 5, прирост относительно четверга
SATURDAY // Субботе присваивается 6, прирост относительно пятницы
)

func main() {
fmt.Println(MONDAY, WEDNESDAY, FRIDAY) // Вывод: 1 3 5
}

Перепишем первый пример, применив пользовательский тип int:

package main

import "fmt"

type Day int

const (
Sunday Day = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)

func main() {
day := Monday
fmt.Println("Today is ", day) // Вывод: Today is 1
}

Здесь определяется пользовательский тип Day, значения констант автоматически увеличиваются с помощью iota. Этот подход лаконичнее и типобезопаснее.

Чтобы константы начинались с 1, пишем iota + 1.

Проверка перечислений структурными тегами

Здесь не нужно определять переменную или константу, просто наследуем функциональность перечисления с помощью структурных тегов, вместо Gin сгодится любой фреймворк Go:

package main

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
)

// Task — это задача со статусом
type Task struct {
Status string `json:"status" binding:"oneof=active completed archived"`
}

func main() {
// Инициализируем маршрутизатор Gin
router := gin.Default()

// Чтобы создать задачу, определяем маршрут
router.POST("/tasks", createTask)

// Запускаем сервер
router.Run(":8080")
}

// Обработчик для создания новой задачи
func createTask(c *gin.Context) {
var task Task

// Привязываем тело запроса JSON к структуре Task
if err := c.BindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Обрабатываем задачу, нам достаточно вывести статус
fmt.Println("New task created with status:", task.Status)

// Отвечаем сообщением об успешном выполнении
c.JSON(http.StatusCreated, gin.H{"message": "Task created successfully"})
}

В этом фрагменте структура Task определяется полем Status. Функцией привязки Gin здесь указывается, что в поле Status должно быть одно из значений: active, completed или archived. Если вызвать API с любыми другими, вернется ошибка: полем Status принимаются только указанные значения.

Проверьте это на двух тестовых сценариях:

  1. { "status" : "deleted"}.
  2. { "status" : "active"}.

Проверка перечислений пользовательскими функциями валидатора

package main

import (
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
)

// Status — это статус задачи
type Status string

const (
Active Status = "active"
Completed Status = "completed"
Archived Status = "archived"
)

// Task — это задача со статусом
type Task struct {
Status Status `json:"status"`
}

// Статус задачи проверяется с помощью ValidateStatus
func ValidateStatus(status Status) error {
switch status {
case Active, Completed, Archived:
return nil
default:
return errors.New("invalid status")
}
}

// Создание новой задачи обрабатывается в CreateTask
func CreateTask(c *gin.Context) {
var task Task

// Привязываем тело запроса JSON к структуре Task
if err := c.BindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Проверяем статус задачи
if err := ValidateStatus(task.Status); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Обрабатываем задачу, нам достаточно вывести статус
fmt.Println("New task created with status:", task.Status)

// Отвечаем сообщением об успешном выполнении
c.JSON(http.StatusCreated, gin.H{"message": "Task created successfully"})
}

func main() {
// Инициализируем маршрутизатор Gin
router := gin.Default()

// Чтобы создать задачу, определяем маршрут
router.POST("/tasks", CreateTask)

// Запускаем сервер
router.Run(":8080")
}
  • В этом коде функцией ValidateStatus проверяется статус задачи; если он недопустимый, возвращается ошибка.
  • Прежде чем обработать задачу, для проверки допустимости ее статуса внутри функции CreateTask вызывается ValidateStatus. Если статус недопустимый, из API вернется ответ на неверный запрос с сообщением об ошибке. В противном случае обрабатываем задачу, как обычно.

Проверьте это поведение на приведенных выше тестовых сценариях.

Заключение

Разработчики всегда стремятся сделать надежную систему, которая не сломается даже при вводе неожиданных значений.

С перечислениями повышаются надежность, гибкость, удобство применения приложений Go, и допускаются только те входные значения, которые были определены.

Вам остается лишь адаптировать эти методы под конкретные сценарии и требования.

Читайте также:

Читайте нас в Telegram, VK и Дзен

Перевод статьи Nidhi D: How to use Enums in Golang

--

--