Các vấn đề của Go Enum và cách giải quyết với xybor-x/enum

0 0 0

Người đăng: Ngoc Huy

Theo Viblo Asia

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

Bình luận

Bài viết tương tự

- vừa được xem lúc

gRPC - Nó là gì và có nên sử dụng hay không?

Nhân một ngày rảnh rỗi, mình ngồi đọc lại RPC cũng như gRPC viết lại để nhớ lâu hơn. Vấn đề là gì và tại sao cần nó .

0 0 132

- vừa được xem lúc

Embedded Template in Go

Getting Start. Part of developing a web application usually revolves around working with HTML as user interface.

0 0 57

- vừa được xem lúc

Tạo Resful API đơn giản với Echo framework và MySQL

1. Giới thiệu.

0 0 61

- vừa được xem lúc

Sử dụng goquery trong golang để crawler thông tin các website Việt Nam bị deface trên mirror-h.org

. Trong bài viết này, mình sẽ cùng mọi người khám phá một package thu thập dữ liệu có tên là goquery của golang. Mục tiêu chính của chương trình crawler này sẽ là lấy thông tin các website Việt Nam bị deface (là tấn công, phá hoại website, làm thay đổi giao diện hiển thị của một trang web, khi người

0 0 237

- vừa được xem lúc

Tạo ứng dụng craw dữ liệu bing với Golang, Mysql driver

Chào mọi người . Lâu lâu ta lại gặp nhau 1 lần, để tiếp tục series chia sẻ kiến thức về tech, hôm nay mình sẽ tìm hiểu và chia sẻ về 1 ngôn ngữ đang khá hot trong cộng đồng IT đó là Golang.

0 0 76

- vừa được xem lúc

Golang: Rest api and routing using MUX

Routing with MUX. Let's create a simple CRUD api for a blog site. # All . GET articles/ .

0 0 55