Go 的枚举很糟糕

Go 对枚举的处理——其实只是带有 `iota` 辅助的整数常量——被批评为与 Rust、Ada、Java,甚至 1970 年代的 Pascal 等语言中更丰富的枚举或和类型系统相比,过于原始且不安全。评论者争论 Go 是否应该加入真正的、类型安全的枚举,提供穷尽性检查和更安全的序列化,还是说它“直接用整数,必要时再用代码生成/Protobuf”的极简哲学,正是为了在普通 Web 和后端工作中保持简单与易用而做出的有意且可接受的权衡。讨论还延伸到更广泛的类型安全、错误处理,以及一门语言在增加特性与保持核心精简和有主见之间应走多远。

“枚举”究竟是什么(枚举 vs 和类型)

  • 若干评论区区分了经典枚举(有限个具名常量的集合,通常有整数底层表示并支持自然迭代/排序)与代数/和类型(可以承载结构化数据的变体,且需要模式匹配)。
  • Rust 的 enum 反复被提到其实是和类型,而不是传统意义上的“真正枚举”,不过也有人认为这种区分在实践中并不太有用。
  • 有些人认为把枚举和和类型强行归为一个概念会让人困惑,并在概念上有害;另一些人则更关注它们共同的“在多个选项中做选择”这一用途。

Go 目前的“枚举”现状

  • Go 没有专门的 enum 关键字;惯用写法是 type T int 再配合 const (...)iota
  • 批评点:
    • 不是封闭集合;任何底层 int 都能构造出来,包括非法值。
    • 没有内置字符串表示、switch 的穷尽性检查,也无法在底层类型之外阻止不同“枚举”类型的混用。
    • 有人认为 iota 对序列化值很危险,因为重排常量会悄悄改变数值编码。
  • 辩护点:
    • 很多人觉得 iota 简洁而且非常方便,尤其适合位标志。
    • 有人认为 Go 的“枚举”就是真正的枚举(区别类型中的具名常量),额外的护栏并不值得增加复杂度。
    • 把任意整数传进来,在一些人看来显然是调用方的 bug,而不是类型系统失败。

替代方案与工具

  • 常见做法:使用代码生成器(go generate、第三方工具、自定义 DSL)来补充字符串化、JSON/Protobuf 集成以及校验。
  • 如果本来就使用 gRPC/Protobuf,Protobuf 枚举被认为是一个稳健的选择,包括安全演进和跨语言支持。

更广泛的语言设计争论

  • 一方称赞 Go 的极简主义、快速构建、简单工具链,以及它对 REST/DevOps 工作的“蓝领”适配性。
  • 另一方则称这门语言原始且“愚蠢地固执”,抗拒公认的特性(泛型、真正的枚举、和类型、空值安全、更丰富的类型约束)。
  • 对比指出,许多更老或其他语言(Pascal、Ada、Rust、ML 语言、C#、Java)拥有更强的枚举或 ADT,在编译期穷尽性检查和与类型系统的整合上更好。

错误处理与可选性

  • 有人认为 Go 显式返回 error 很合理,把错误当作正常控制流。
  • 另一些人更偏好 Result/Either/Option 风格的和类型,并认为 Go 缺乏这类构造(再加上到处都是 nil 可空性)是一个重大的设计缺口。