深圳幻海软件技术有限公司 欢迎您!

Go 语言里怎么正确实现枚举?答案藏着官方的源码里

2023-02-28

本文转载自微信公众号「网管叨bi叨」,作者网管。转载本文请联系网管叨bi叨公众号。在编程领域里,枚举是用来表示只包含有限数量的固定值的类型,在开发中一般用于标识错误码或者状态机。拿一个实体对象的状态机来说,它通常与这个对象在数据库里对应记录的标识状态的字段值相对应。在刚开始学编程的时候,你一定写过,

本文转载自微信公众号「网管叨bi叨」,作者网管。转载本文请联系网管叨bi叨公众号。

在编程领域里,枚举是用来表示只包含有限数量的固定值的类型,在开发中一般用于标识错误码或者状态机。拿一个实体对象的状态机来说,它通常与这个对象在数据库里对应记录的标识状态的字段值相对应。

在刚开始学编程的时候,你一定写过,至少见过直接使用魔术数字进行判断的代码。啥叫魔术数字呢,举个例子,要置顶一个文章的时候先判断文章是不是已发布状态。

if (article.state == 2) { 
   // state 2 代表文章已发布 

  • 1.
  • 2.
  • 3.

假如我们的代码里没有注释,或者等我们项目的代码里充斥着这些魔术数字的判断的时候,你是不是会很头疼?

后来我就学会了把这些状态值定义成常量,并且也搞一个判断对象状态的方法单独封装这段逻辑。

public class ArticleState { 
     
    public static final int Draft = 1; //草稿 
     
    public static final int Published = 2; //发布 
     
    public static final int Deleted = 3; // 已删除 

 
public  Boolean checkArticleState(int state) { 
     
    ... 
     

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

这种用法,肯定是比在程序里直接用魔术数字进行判断要强很多啦,至少看着不会很头疼,不会想骂**。

不过后来被当时带我的老大哥说这种也有缺点,上面这个 checkArticleState 方法用来检查文章状态,本意是让调用者传入 ArticleState 的三个静态常量之一,但由于没有类型上的约束,因此传入任意一个 int 值在语法上也是允许的,编译器也不会提出任何警告,换成用枚举更合适一些。

em~! 我不记得大学教 Java 的那个学期老师讲过这玩意啊,莫非又是一个上课玩手机错过的知识点?......

所以使用枚举后我们的Java代码变成了:

// 使用enum而非class声明 
public enum ArticleState { 
  
    //要在enum里创建所有的枚举对象 
    Draft(1, "草稿"); 
    Published(2, "已发布"); 
    Deleted(3, "已删除"
       
    // 自定义属性 
    private int code; 
 
    private String text; 
   
    // 构造方法必须是private的 
    ArticleState(int code, String text) { 
        this.code = id; 
        this.text = name
    } 

 
public  Boolean checkArticleState(ArticleState state) { 
     
    ... 
     

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

这样就能靠形参的枚举类型帮我们过滤掉非法的状态值。把整型值作为参数传给 checkArticleState 方法时因为类型不匹配编译不过去,在写代码时编译器也能马上提示出来。

如果没有用过 Java 的小伙伴也不用纠结,主要的语法点我用注释标注出来了。

后来这两年主要在用Go做项目,我发现相似的问题 Go 里也存在,但是 Go 并没有提供枚举类型,那怎么做到进行状态值的正确限制呢?如果还是用 int 型的常量肯定不行。比如:

const ( 
    Draft int = 1 
    Published = 2 
    Deleted   = 3 

 
const ( 
    Summer int = 1 
    Autumn     = 2 
    Winter     = 3 
    Spring     = 4 

 
func main() { 
    // 输出 true, 不会有任何编译错误 
    fmt.Println(Autumn == Draft) 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

比如上面定义了两组 int 类型的常量,一类代表文章状态,一类代表季节的四季。这种方式拿文章状态与季节进行比较不会有任何编译上的错误。

答案在 Go 内置库或者一些咱们都知道的开源库的代码里就能找到。比如看看 google.golang.org/grpc/codes 里的gRPC 的错误码是怎么定义的,我们马上就能明白该怎么正确的实现枚举。

下面不多卖关子直接上答案了,不想去源码里看的,就看我这里写的也行,都是这么做的。

我们可以用 int 作为基础类型创建一个别名类型,Go 里边是支持这个的

type Season int 
 
const ( 
 Summer Season = 1 
 Autumn        = 2 
 Winter        = 3 
 Spring        = 4 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

当然定义连续的常量值的时候 Go 里边经常使用 iota,所以上面的定义还能进一步简化。

type Season int 
 
const ( 
 Summer Season = iota + 1 
 Autumn 
 Winter 
 Spring 

 
type ArticleState int 
 
const ( 
  Draft ArticleState = iota + 1 
  Published 
  Deleted   

 
func checkArticleState(ArticleState state) { 
 // ...  

 
 func main() { 
   // 两个操作数类型不匹配,编译错误 
   fmt.Println(Autumn == Draft) 
   // 参数类型不匹配,编译错误 
   checkArticleState(100) 
 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

虽然这些状态值的底层的类型都是 int 值,但是现在不论是进行两个不相干类型的枚举值比较,还是用整型值作为参数调用 checkArticleState 方法检查文章状态,都会造成编译错误,因为现在我们使用状态值的地方都有了类型限制。

这就是为什么针对错误码、状态机这种涉及有限数量状态值的场景下不能用整型常量而是要用枚举的原因。虽然 Go 语言里没有像 Java 一样单独提供一个 enum 表示枚举的类型,但是我们仍然能通过创建类型别名来实现枚举。

 

你学会了吗?(#^.^#)