前言
Go 既沒有 Enum(枚舉)也沒有 Union(聯合)的關鍵字,因此會使用現有的語言特性來達成相同的效果。藉由研究其他語言如 TypeScript 來了解如何重現相同的特性。
TypeScript 為例
Enum
通常會用 Enum 關鍵字來表達一組常數,細節可以參考:enum、const enum 和 as const,應該如何列舉資料於 TypeScript 當中?
enum ApiStatus { Idle, Loading, Success, Error}Union
在 TypeScript 通常會用聯合型別表達一系列的型別:
type ApiState = | { type: 'idle' } | { type: 'loading' } | { type: 'success'; data: Book[] } | { type: 'error'; message: string };Go 為例
Enum
Go 沒有 enum 關鍵字,慣用的做法是結合 iota 與自訂型別來模擬 Enum:
type ApiStatus int
const ( Idle ApiStatus = iota Loading Success Error)iota 會從 0 開始依序遞增,因此 Idle = 0、Loading = 1,以此類推。若想讓 Enum 值更具可讀性,也可以改用字串型別:
type ApiStatus string
const ( Idle ApiStatus = "idle" Loading ApiStatus = "loading" Success ApiStatus = "success" Error ApiStatus = "error")func handleStatus(status ApiStatus) { switch status { case Idle: fmt.Println("尚未開始") case Loading: fmt.Println("載入中...") case Success: fmt.Println("載入成功") case Error: fmt.Println("發生錯誤") }}Union
Go 沒有原生的 Union 型別,慣用的做法是透過 interface 搭配 Struct 來模擬,每種狀態各自定義 Struct 並實作同一個 interface:
type ApiState interface { apiState()}
type IdleState struct{}type LoadingState struct{}type SuccessState struct{ Data []Book }type ErrorState struct{ Message string }
func (IdleState) apiState() {}func (LoadingState) apiState() {}func (SuccessState) apiState() {}func (ErrorState) apiState() {}使用時搭配 type switch 來對不同狀態進行處理:
func handle(state ApiState) { switch s := state.(type) { case IdleState: fmt.Println("idle") case LoadingState: fmt.Println("loading") case SuccessState: fmt.Printf("success: %v\n", s.Data) case ErrorState: fmt.Printf("error: %s\n", s.Message) }}這個模式的優點是型別安全,且每種狀態可以攜帶各自的資料(如 SuccessState 的 Data 或 ErrorState 的 Message),與 TypeScript 的 Discriminated Union 概念相近或是說這是一種達成 Sum Type 的手段。
總結
- Enum 組合 (const + iota):用在 「純標籤、無資料」 的場景(例如:星期一到星期日、相關聯的數值清單)。
- Union 組合 (interface + struct):用在 「不同狀態需要夾帶不同資料」 的場景(例如:API 成功與失敗的結果、不同輸入情境)。
- 與 TypeScript 不同,Go 的 interface 是隱性實踐的設計,任何型別只要實作對應方法,就能成為該 interface 的實作,因此 compiler 無法得知所有可能的型別集合。這代表 Go 無法像 TypeScript 一樣進行 exhaustiveness checking(窮舉檢查),也就是 compiler 不會強制開發者在 type switch 中處理所有狀態。因此新增新的 state 時,既有的 type switch 可能會忽略該型別,而不會產生編譯錯誤。
// 一種方法是明確捕捉未知的型別default:panic("unreachable")
延伸閱讀
- Implementing Discriminated Unions in Go: A TypeScript Perspective - Daniel Schmidt’s Blog
- TypeScript Enum’s vs Discriminated Unions - Software, Fitness, and Gaming – Jesse Warden