DEV Community

Cover image for Go Enum’s problems and solutions with xybor-x/enum
Ngoc Huy
Ngoc Huy

Posted on

Go Enum’s problems and solutions with xybor-x/enum

#go

What is enum?

An enum, short for enumeration, is a special data type that represents a set of named values. It is used to define a collection of constant values that are conceptually related, improving code readability and reducing errors caused by the use of arbitrary literal values.

// Enum in Java
enum TrafficLight {
    RED, YELLOW, GREEN
}
Enter fullscreen mode Exit fullscreen mode
# Enum in Python
from enum import Enum

class TrafficLight(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3
Enter fullscreen mode Exit fullscreen mode

Enum in Go

Go doesn’t support enum natively. However, there is a popular way to define an enum in Go is using iota approach.

package main

type TrafficLight int

const (
    RED TrafficLight = iota // 0
    GREEN                   // 1
    BLUE                    // 2
)

func main() {
    fmt.Println(RED) // Output: 0
}
Enter fullscreen mode Exit fullscreen mode

However, there are some problems when dealing with enum in this way:

  • Lack of Built-in Methods: No direct support for features like listing all enum values or converting between strings and enums.
  • Limited Type Safety: Enums are typically represented using basic types (e.g., int or string), which increases the risk of unintended assignments.
  • Serialization and Deserialization Complexity: Mapping enums to and from formats like JSON requires additional handling.

The xybor-x/enum library

go enum

The xybor-x/enum libray provides elegant, easy-to-use, and powerful solutions for Go enum with no code generation.

There are some types of enum which you can work with xybor-x/enum, please choose the most suitable one.

Basic enum

Pros 💪

  • Simple.
  • Supports constant values.

Cons 👎

  • No built-in methods.
  • No type safety.
  • Lacks serialization and deserialization support. Like the traditional enum, the basic enum has no built-in methods. But you can use utility functions of xybor-x/enum to handle this type of enum.
package main

type Role int

const (
    RoleUser Role = iota
    RoleAdmin
)

func init() {
    enum.Map(RoleUser, "user")
    enum.Map(RoleAdmin, "admin")

    // Optional: ensure no new enum values can be added to Role.
    enum.Finalize[Role]()
}

func main() {
    // Print the corresponding string.
    fmt.Println(enum.ToString(RoleUser)) // Output: user

    // Print out all valid enums.
    fmt.Println(enum.All[Role]())       // Output: [0 1]

    // Parse an enum from int.
    r1, ok := enum.FromInt[Role](1)
    fmt.Println(ok)                // Output: true
    fmt.Println(enum.ToString(r1)) // Output: admin

    // Parse an enum from string.
    r2, ok := enum.FromString[Role]("admin")
    fmt.Println(ok) // Output: true
    fmt.Println(r2) // Output: 1

    // Serialize json.
    data, err := enum.MarshalJSON(RoleUser)
    fmt.Println(err)          // Output: nil
    fmt.Println(string(data)) // Output: "user"
}
Enter fullscreen mode Exit fullscreen mode

WrapEnum

Pros 💪

  • Supports constant values.
  • Provides many useful built-in methods.
  • Full serialization and deserialization support out of the box.

Cons 👎

  • Provides only basic type safety.
package main

// Only need to change the two following lines fromthe Basic enum.
type role any
type Role = enum.WrapEnum[role]

const (
    RoleUser Role = iota
    RoleAdmin
)

func init() {
    enum.Map(RoleUser, "user")
    enum.Map(RoleAdmin, "admin")

    // Optional: ensure no new enum values can be added to Role.
    enum.Finalize[Role]()
}

func main() {
    // Print the corresponding string. No need to use enum.ToString.
    fmt.Println(RoleUser) // Output: user

    // Print out all valid enums.
    fmt.Println(enum.All[Role]())       // Output: [user admin]

    // Parse an enum from int.
    r1, ok := enum.FromInt[Role](1)
    fmt.Println(ok) // Output: true
    fmt.Println(r1) // Output: admin

    // Parse an enum from string.
    r2, ok := enum.FromString[Role]("admin")
    fmt.Println(ok) // Output: true
    fmt.Println(r2) // Output: admin

    // Now you can use json.Marshal instead of enum.MarshalJSON.
    data, err := json.Marshal(RoleUser)
    fmt.Println(err)          // Output: nil
    fmt.Println(string(data)) // Output: "user"
}
Enter fullscreen mode Exit fullscreen mode

WrapEnum is the most suitable enum for general cases. However, it only provides basic type safety. If you want a stricter one, consider using SafeEnum.

// WrapEnum cannot prevent this type of invalid declaration.
// Consider using SafeEnum.
r := Role(42)
Enter fullscreen mode Exit fullscreen mode

SafeEnum

SafeEnum defines a strong type-safe enum. Like WrapEnum, it provides a set of built-in methods to simplify working with enums.

The SafeEnum enforces strict type safety, ensuring that only predefined enum values are allowed. It prevents the accidental creation of new enum types, providing a guaranteed set of valid values.

Pros 💪

  • Provides strong type safety.
  • Provides many useful built-in methods.
  • Full serialization and deserialization support out of the box.

Cons 👎

  • Does not support constant values.

Why is constant-support important?

Some static analysis tools (such as nogo for bazel, golangci-lint with exhaustive extension) support checking for exhaustive switch statements in constant enums. By choosing an enum with constant support, you can enable this functionality in these tools.

package main

type role any
type Role = enum.SafeEnum[role]

var (
    RoleUser  = enum.NewSafe[Role]("user")
    RoleAdmin = enum.NewSafe[Role]("admin")

    // Optional: ensure no new enum values can be added to Role.
    enum.Finalize[Role]()
)

func main() {
    // You cannot create an enum like that, it causes a compile-time error.
    // r := Role(42)
    // r := Role("moderator")

    // Print the corresponding string. No need to use enum.ToString.
    fmt.Println(RoleUser) // Output: user

    // Print out all valid enums.
    fmt.Println(enum.All[Role]())       // Output: [user admin]

    // Parse an enum from int.
    r1, ok := enum.FromInt[Role](1)
    fmt.Println(ok) // Output: true
    fmt.Println(r1) // Output: admin

    // Parse an enum from string.
    r2, ok := enum.FromString[Role]("admin")
    fmt.Println(ok) // Output: true
    fmt.Println(r2) // Output: admin

    // Now you can use json.Marshal instead of enum.MarshalJSON.
    data, err := json.Marshal(RoleUser)
    fmt.Println(err)          // Output: nil
    fmt.Println(string(data)) // Output: "user"
}
Enter fullscreen mode Exit fullscreen mode

References

xybor-x/enum: https://github.com/xybor-x/enum

Medium: https://medium.com/@huykingsofm/enum-handling-in-go-a2727154435e
Vietnamese viblo: https://viblo.asia/p/cac-van-de-cua-go-enum-va-cach-giai-quyet-voi-xybor-xenum-Yym401A9J91

Top comments (0)