Как использовать перечисления в Golang
Введение
В 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
принимаются только указанные значения.
Проверьте это на двух тестовых сценариях:
{ "status" : "deleted"}
.{ "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, и допускаются только те входные значения, которые были определены.
Вам остается лишь адаптировать эти методы под конкретные сценарии и требования.
Читайте также:
Перевод статьи Nidhi D: How to use Enums in Golang