Các vấn đề của Go Enum và cách giải quyết với xybor-x/enum
Enum là gì?
Enum (hay enumeration) là một kiểu dữ liệu đặc biệt, phổ biến trong các ngôn ngữ lập trình. Nó là một tập hợp hữu hạn các hằng số có liên quan với nhau về mặt khái niệm. Ví dụ:
- Một tuần có các ngày thứ hai, thứ ba, thứ tư, thứ năm, thứ sáu, thứ bảy, chủ nhât.
- Vai trò của một user trong hệ thống có thể là người dùng bình thường, quản trị viên.
Biểu diễn trong ngôn ngữ lập trình, nó có dạng như sau:
enum Weekday {
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
enum Role {
User, Admin
}
Enum trong Golang
Go không chính thức hỗ trợ enum. Tuy nhiên, ta thường thấy các Gopher sử dụng iota
để định nghĩa enum như sau:
type Role int
const (
RoleUser Role = iota // RoleUser = 0
RoleAdmin // RoleAdmin = 1
)
Cách này sẽ tạo ra 2 hằng số là RoleUser
với giá trị là 0, và RoleAdmin
với giá trị là 1. Cách này thường được sử dụng phổ biến vì sự đơn giản của nó. Tuy nhiên nó có rất nhiều nhược điểm nếu so với enum của các ngôn ngữ khác như Java, C#, Python:
- Không có các function, method hỗ trợ: Vì
Role
chỉ tái định nghĩa kiểuint
, một kiểu nguyên thủy, nên nó không có sẵn bất kỳ phương thức hữu ích nào. Ngoài ra, nó còn khó để lấy ra tất cả các giá trị enum, hay kiểm tra một giá trị enum là hợp lệ hay không. - Không đảm bảo type-safe: Chúng ta có thể tạo ra một
Role
không hợp lệ ngay tại runtime mà không có bất kỳ cảnh báo nào, cũng không có cách để kiểm tra sự hợp lệ của nó, ví dụr := Role(42)
. - Thiếu đi khả năng serialization và deserialization: Khi chuyển sang dạng JSON hoặc đưa vào database, nó sẽ được biểu diễn dưới dạng số nguyên, chứ không phải là
user
hayadmin
. Dẫn đến thiếu tính trực quan.
Thư viện xybor-x/enum
Thư viện xybor-x/enum
cung cấp các giải pháp để xử lý enum một cách mạnh mẽ trong Go, mà không cần sinh code (no code generation).
Có nhiều loại enum vớí ưu và nhược điểm khác nhau, bạn có thể chọn loại enum phù hợp nhất.
Basic enum
Ưu điểm:
- Đơn giản.
- Tương thích với
iota
enum.
Nhược điểm:
- Không type-safe.
- Không có serialization và deserialization.
- Không có sẵn method hỗ trợ.
Giống với enum truyền thống, kiểu này cũng không có sẵn các method hỗ trợ, tuy nhiên, bạn có thể sử dụng các hàm tiện ích của thư viện xybor-x/enum
để tương tác với chúng.
type Role int
const (
RoleUser Role = iota
RoleAdmin
)
func init() {
enum.Map(RoleUser, "user")
enum.Map(RoleAdmin, "admin")
// Mục đích để đảm bảo không có thêm giá trị nào được thêm vào enum Role.
enum.Finalize[Role]()
}
func main() {
// In ra kiểu string tương ứng.
fmt.Println(enum.ToString(RoleUser)) // Output: user
// In ra tất cả các enum hợp lệ.
fmt.Println(enum.All[Role]()) // Output: [0 1]
// Chuyển đổi từ kiểu int.
r1, ok := enum.FromInt[Role](1)
fmt.Println(ok) // Output: true
fmt.Println(enum.ToString(r1)) // Output: admin
// Chuyển đổi từ kiểu string.
r2, ok := enum.FromString[Role]("admin")
fmt.Println(ok) // Output: true
fmt.Println(r2) // Output: 1
// Serialize sang json.
data, err := enum.MarshalJSON(RoleUser)
fmt.Println(err) // Output: nil
fmt.Println(string(data)) // Output: "user"
}
WrapEnum
Ưu điểm:
- Tương thích với
iota
enum. - Hỗ trợ serialization và deserialization.
- Có sẵn các methods hỗ trợ.
Nhược điểm:
- Chỉ cung cấp type-safe ở mức căn bản.
// Chỉ cần đổi hai dòng này so với Basic enum.
type role int
type Role = enum.WrapEnum[role]
const (
RoleUser Role = iota
RoleAdmin
)
func init() {
enum.Map(RoleUser, "user")
enum.Map(RoleAdmin, "admin")
// Mục đích để đảm bảo không có thêm giá trị nào được thêm vào enum Role.
enum.Finalize[Role]()
}
func main() {
// WrapEnum đã implement sẵn nhiều method, do đó không cần dùng tới
// các hàm hỗ trợ.
// In ra kiểu string tương ứng.
fmt.Println(RoleUser) // Output: user
// In ra tất cả các enum hợp lệ.
fmt.Println(enum.All[Role]()) // Output: [user admin]
// Chuyển đổi từ kiểu int.
r1, ok := enum.FromInt[Role](1)
fmt.Println(ok) // Output: true
fmt.Println(r1) // Output: admin
// Chuyển đổi từ kiểu string.
r2, ok := enum.FromString[Role]("admin")
fmt.Println(ok) // Output: true
fmt.Println(r2) // Output: admin
// Serialize sang json. Có thể dùng luôn json.Marshal ở đây,
// thay vì enum.MarshalJSON.
data, err := json.Marshal(RoleUser)
fmt.Println(err) // Output: nil
fmt.Println(string(data)) // Output: "user"
}
WrapEnum
là kiểu enum được khuyến khích sử dụng nhất vì nó có nhiều tiện ích, tương thích với iota
(hỗ trợ hằng số), có sẵn serialization và deserialization. Tuy nhiên, nó chỉ cung cấp type-safe ở mức cơ bản (do đã implement sẵn các phương thể serialization và deserialization, nên sẽ giảm đi các trường hợp enum không hợp lệ). Nếu bạn cần một kiểu enum với type-safe ở mức mạnh mẽ, hay sử dụng SafeEnum
.
// WrapEnum vẫn không thể ngăn chặn được kiểu khai báo này.
r := Role(42)
SafeEnum
Ưu điểm:
- Cung cấp type-safe ở mức cao.
- Hỗ trợ serialization và deserialization.
- Có sẵn các methods hỗ trợ.
Nhược điểm:
- Không tương thích với
iota
enum (không hỗ trợ constant).
Tại sao hỗ trợ constant lại quan trọng?
Một số công cụ phân tích (như
nogo for bazel
, golangci-lint vớiexhaustive linter
) có thể kiểm tra ̣câu lệnhswitch
đã bao gồm tất cả các enum hay chưa. Tuy nhiên nó chỉ hoạt động cho các constant enum, nếu bạn KHÔNG sử dụng các công cụ này, có thể xem xét sử dụngSafeEnum
để đạt được mức độ cao hơn của type safety.
type role int
type Role = enum.SafeEnum[role]
var (
RoleUser = enum.NewSafe[Role]("user")
RoleAdmin = enum.NewSafe[Role]("admin")
// Mục đích để đảm bảo không có thêm giá trị nào được thêm vào enum Role.
enum.Finalize[Role]()
)
func main() {
// SafeEnum không cho phép bạn tạo mới các giá trị enum không hợp lệ.
// r := Role(42) hoặc r := Role("mod") đều bị lỗi cú pháp.
// Ngoài ra, SafeEnum hoàn toàn giống với WrapEnum.
// In ra kiểu string tương ứng.
fmt.Println(RoleUser) // Output: user
// In ra tất cả các enum hợp lệ.
fmt.Println(enum.All[Role]()) // Output: [user admin]
// Chuyển đổi từ kiểu int.
r1, ok := enum.FromInt[Role](1)
fmt.Println(ok) // Output: true
fmt.Println(r1) // Output: admin
// Chuyển đổi từ kiểu string.
r2, ok := enum.FromString[Role]("admin")
fmt.Println(ok) // Output: true
fmt.Println(r2) // Output: admin
// Serialize sang json. Có thể dùng luôn json.Marshal ở đây,
// thay vì enum.MarshalJSON.
data, err := json.Marshal(RoleUser)
fmt.Println(err) // Output: nil
fmt.Println(string(data)) // Output: "user"
}
References
Thư viện xybor-x/enum
: https://github.com/xybor-x/enum
Dev.to: https://dev.to/huykingsofm/go-enums-problems-and-solutions-with-xybor-xenum-3e79
Medium: https://medium.com/@huykingsofm/enum-handling-in-go-a2727154435e
All rights reserved