Idiomatic Go: Enum and Union

地道 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("空闲")
case LoadingState:
fmt.Println("加载中")
case SuccessState:
fmt.Printf("成功:%v\n", s.Data)
case ErrorState:
fmt.Printf("错误:%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("不可到达")

延伸阅读