Go Enum

道地 Go 語言 Enum 與 Union 實踐方式

前言

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 = 0Loading = 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)
}
}

這個模式的優點是型別安全,且每種狀態可以攜帶各自的資料(如 SuccessStateDataErrorStateMessage),與 TypeScript 的 Discriminated Union 概念相近或是說這是一種達成 Sum Type 的手段。

總結

  1. Enum 組合 (const + iota):用在 「純標籤、無資料」 的場景(例如:星期一到星期日、相關聯的數值清單)。
  2. Union 組合 (interface + struct):用在 「不同狀態需要夾帶不同資料」 的場景(例如:API 成功與失敗的結果、不同輸入情境)。
    • 與 TypeScript 不同,Go 的 interface 是隱性實踐的設計,任何型別只要實作對應方法,就能成為該 interface 的實作,因此 compiler 無法得知所有可能的型別集合。這代表 Go 無法像 TypeScript 一樣進行 exhaustiveness checking(窮舉檢查),也就是 compiler 不會強制開發者在 type switch 中處理所有狀態。因此新增新的 state 時,既有的 type switch 可能會忽略該型別,而不會產生編譯錯誤。
    // 一種方法是明確捕捉未知的型別
    default:
    panic("unreachable")

延伸閱讀