Elegant, powerful, and dependency-free enums for Go with zero code generation!
go get -u github.com/xybor-x/enum
Tip
xybor-x/enum
supports three enum types: Basic enum for simplicity, Wrap enum for enhanced functionality, and Safe enum for strict type safety.
Basic enum (#) | Wrap enum (#) | Safe enum (#) | |
---|---|---|---|
Underlying type required | No | Yes | Yes |
Built-in methods | No | Yes | Yes |
Constant enum (#) | Yes | Yes | No |
Serialization and deserialization (#) | No | Yes | Yes |
Type safety (#) | No | Basic | Strong |
Used with Nullable (#) | Yes | Yes | Yes |
Integrate with protobuf enum (#) | Yes | Yes | Yes |
The basic enum (a.k.a iota
enum) is the most commonly used enum implementation in Go.
It is essentially a primitive type, which does not include any built-in methods. For handling this type of enum, please refer to the utility functions.
Pros 💪
- Simple.
- Supports constant values (#).
Cons 👎
- No built-in methods.
- No type safety (#).
- Lacks serialization and deserialization support.
type Role int
const (
RoleUser Role = iota
RoleAdmin
)
func init() {
enum.Map(RoleUser, "user")
enum.Map(RoleAdmin, "admin")
enum.Finalize[Role]()
}
WrapEnum
offers a set of built-in methods to simplify working with int
enums.
Tip
For other numeric types, use WrapUintEnum
for uint
and WrapFloatEnum
for float64
.
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 (#).
// Define enum's underlying type.
type role any
// Create a WrapEnum type for roles.
type Role = enum.WrapEnum[role] // NOTE: It must use type alias instead of type definition.
const (
RoleUser Role = iota
RoleAdmin
)
func init() {
enum.Map(RoleUser, "user")
enum.Map(RoleAdmin, "admin")
enum.Finalize[Role]()
}
func main() {
// WrapEnum has many built-in methods for handling enum easier.
data, _ := json.Marshal(RoleUser) // Output: "user"
fmt.Println(RoleAdmin.IsValid()) // Output: true
}
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 (#).
// Define enum's underlying type.
type role any
// Create a SafeEnum type for roles.
type Role = enum.SafeEnum[role] // NOTE: It must use type alias instead of type definition.
var (
RoleUser = enum.New[Role]("user")
RoleAdmin = enum.New[Role]("admin")
_ = enum.Finalize[Role]()
)
func main() {
// SafeEnum has many built-in methods for handling enum easier.
data, _ := json.Marshal(RoleUser) // Output: "user"
fmt.Println(RoleAdmin.IsValid()) // Output: true
}
Note
All of the following functions can be used with any type of enum. See more functions here.
FromString
FromString
returns the corresponding enum
for a given string
representation, and whether it is valid.
role, ok := enum.FromString[Role]("user")
if ok {
fmt.Println(role) // Output: 0
} else {
fmt.Println("Invalid enum")
}
FromNumber
FromNumber
returns the corresponding enum
for a given numeric representation, and whether it is valid.
role, ok := enum.FromNumber[Role](42)
if ok {
fmt.Println(role)
} else {
fmt.Println("Invalid enum") // Output: Invalid enum
}
IsValid
IsValid
checks if an enum value is valid or not.
fmt.Println(enum.IsValid(RoleUser)) // true
fmt.Println(enum.IsValid(Role(0))) // true
fmt.Println(enum.IsValid(Role(42))) // false
ToString
ToString
converts an enum
to string
. It returns <nil>
for invalid enums.
fmt.Println(enum.ToString(RoleAdmin)) // Output: "admin"
fmt.Println(enum.ToString(Role(42))) // Output: "<nil>"
All
All
returns a slice containing all enum values of a specific enum type.
for _, role := range enum.All[Role]() {
fmt.Println("Role:", enum.ToString(role))
}
// Output:
// Role: user
// Role: admin
Some static analysis tools support checking for exhaustive switch
statements in constant enums. By choosing an enum
with constant support, you can enable this functionality in these tools.
Serialization and deserialization are essential when working with enums, and our library provides seamless support for handling them out of the box.
Warning
Not all enum types support serde operations out of the box, please refer to the features.
Currently supported:
JSON
: Implementsjson.Marshaler
andjson.Unmarshaler
.SQL
: Implementsdriver.Valuer
andsql.Scanner
.YAML
: Implementsyaml.Marshaler
andyaml.Unmarshaler
.XML
: Implementsxml.Marshaler
andxml.Unmarshaler
.
The Nullable
transforms an enum type into a nullable enum, akin to sql.NullXXX
, and is designed to handle nullable values in both JSON, YAML, and SQL.
type Role int
type NullRole = enum.Nullable[Role]
type User struct {
ID int `json:"id"`
Role NullRole `json:"role"`
}
func main() {
fmt.Println(json.Marshal(User{})) // {"id": 0, "role": null}
}
The WrapEnum prevents most invalid enum cases due to built-in methods for serialization and deserialization, offering basic type safety.
However, it is still possible to accidentally create an invalid enum value, like this:
moderator := Role(42) // Invalid enum value
The SafeEnum provides strong type safety, ensuring that only predefined enum values are allowed. There is no way to create a new SafeEnum
object without explicitly using the New
function or zero initialization.
moderator := Role(42) // Compile-time error
moderator := Role("moderator") // Compile-time error
Suppose we have an external enum (such as protobuf enum) defined as follows:
// Code generated by protoc-gen-go.
package proto
type Role int32
const (
Role_User Role = 0
Role_Admin Role = 1
)
...
func (x Role) String() string {
...
}
We can integrate them enums into xybor-x/enum
by mapping the enum to extra representations. Here's an example:
package main
import (
"path/to/proto"
"github.com/xybor-x/enum"
)
type Role int
const (
RoleUser Role = iota
RoleAdmin
)
func init() {
// Map each enum value to its string and protobuf representation.
enum.Map(RoleUser, "user", proto.Role_User)
enum.Map(RoleAdmin, "admin", proto.Role_Admin)
enum.Finalize[Role]()
}
func main() {
// Convert the protobuf enum to the Role enum.
role, ok := enum.From[Role](proto.Role_User)
fmt.Println(ok, role) // Output: true user
// Convert the Role enum to the protobuf enum.
fmt.Println(enum.To[proto.Role](RoleUser)) // Output: User
}
We can utilize the String()
method of protobuf enum for the string representation of this enum:
func init() {
// Map to only protobuf enum value (utilize protobuf enum's string representation).
enum.Map(RoleUser, proto.Role_User)
enum.Map(RoleAdmin, proto.Role_Admin)
enum.Finalize[Role]()
}
func main() {
// Convert the protobuf enum to the Role enum. Note that the string
// representation of RoleUser is "User" rather than "user".
role, ok := enum.From[Role](proto.Role_User)
fmt.Println(ok, role) // Output: true User
}
You can also utilize the underlying enum of WrapEnum
s or SafeEnum
for:
- Convenience: Convert the enum to its underlying enum using the
To
method. - Type safety: By defining the underlying enum with exported, non-empty methods,
xybor-x/enum
ensures that all enums must include the underlying representation.
type Role = enum.WrapEnum[proto.Role]
const (
RoleUser Role = iota
RoleAdmin
RoleModerator
)
func init() {
// Map to only protobuf enum value (utilize protobuf enum's string representation).
enum.Map(RoleUser, proto.Role_User)
enum.Map(RoleAdmin, proto.Role_Admin)
// enum.Map(RoleModerator, "moderator")
// panic: enum WrapEnum[Role] (2): require a representation of proto.Role
enum.Finalize[Role]()
}
func main() {
// Convert the Role enum to the protobuf enum with the built-in method.
fmt.Println(RoleAdmin.To()) // Output: Admin
}
Extend basic enum
Since this enum is just a primitive type and does not have built-in methods, you can easily extend it by directly adding additional methods.
type Role int
const (
RoleUser Role = iota
RoleMod
RoleAdmin
)
func init() {
enum.Map(RoleUser, "user")
enum.Map(RoleMod, "mod")
enum.Map(RoleAdmin, "admin")
enum.Finalize[Role]()
}
func (r Role) HasPermission() bool {
return r == RoleMod || r == RoleAdmin
}
Extend WrapEnum
WrapEnum
has many predefined methods. The only way to retain these methods while extending it is to wrap it as an embedded field in another struct.
However, this approach will break the constant-support property of the WrapEnum
because Go does not support constants for structs.
You should consider extending Basic enum or Safe enum instead.
Extend SafeEnum
SafeEnum
has many predefined methods. The only way to retain these methods while extending it is to wrap it as an embedded field in another struct.
xybor-x/enum
provides the NewExtended
function to help create a wrapper of advanced enums.
type role any
type Role struct { enum.SafeEnum[role] }
var (
RoleUser = enum.NewExtended[Role]("user")
RoleMod = enum.NewExtended[Role]("mod")
RoleAdmin = enum.NewExtended[Role]("admin")
_ = enum.Finalize[Role]()
)
func (r Role) HasPermission() bool {
return r == RoleMod || r == RoleAdmin
}