0

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ểu int, 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 hay admin. 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ới exhaustive linter) có thể kiểm tra ̣câu lệnh switch đã 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ụng SafeEnum để đạ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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí