前言
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("空闲") case LoadingState: fmt.Println("加载中") case SuccessState: fmt.Printf("成功:%v\n", s.Data) case ErrorState: fmt.Printf("错误:%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("不可到达")
延伸阅读
- Implementing Discriminated Unions in Go: A TypeScript Perspective - Daniel Schmidt’s Blog
- TypeScript Enum’s vs Discriminated Unions - Software, Fitness, and Gaming – Jesse Warden