Go 100 mistakes之如何正确设置枚举值中的零值

2023-01-31 15:28:49 浏览数 (1)

本文是对 《100 Go Mistackes:How to Avoid Them》 一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。

我们知道,在Go中会给定义的变量一个默认值,比如int类型的变量默认值是0。我们在定义枚举值时,往往也会从0值开始定义。本文就解释如何区分是显示指定了变量的0值还是因为确实字段而得到的默认值。

在编程语言中,枚举类型是由一组值组成的数据类型。在Go语言中,没有enum这样的关键字。然而,处理一组值最好的方法是用类型别名和常量。但是,我们无法达到其他语言所能达到的安全水平。这就是为什么我们在处理枚举值时必须要小心的原因。让我们来看一些相关的实践以及如何避免一些常见的错误。

下面列出了一周中周几的列表:

代码语言:javascript复制
type Weekday int ①const (   Monday    Weekday = 0 ②   Tuesday   Weekday = 1   Wednesday Weekday = 2   Thursday  Weekday = 3   Friday    Weekday = 4   Saturday  Weekday = 5   Sunday    Weekday = 6)

① 定义一个自定义的Weekday类型

② 创建一个Weekday类型的Modany常量

创建一个Weekday类型的好处是可以强制让编译器在编译时做类型检查以及提高可读性。如果我们没有创建一个Weekday类型,那么下面的函数签名对于调用者来说可能会有一点难懂:

代码语言:javascript复制
func GetCurrentWeekday() int {
  // ...
}
代码语言:javascript复制
一个int类型可以包含任何值,同时阅读者如果没有相关的阅读文档或者代码的话也不能猜出该函数返回的是什么值。相反,如果定义一个Weekday类型,那么就会使该函数的签名更清晰可读:

func getCurrentWeekday() Weekday {
  // ...
}
在这个例子中,我们强制指定了返回具体的类型Weekday。

我们创建Weekday类型的枚举值的方法是比较合适的。然而,在Go中,还有一种惯用的方法来声明枚举中的常量,那就是使用常量生成器 iota

注意:在本例中,我们还可以将Weekday声明为uint32,以强制正值并确保每个Weekday变量分配32位。

iota

iota 用于创建一系列相关值,而无需明确设置这些值。它指示编译器复制每个常量表达式,直到块结束或遇到到赋值表达式。

下面是用iota的Weekday版本:

代码语言:javascript复制
type Weekday int
const (
   Monday Weekday = iota ①
   Tuesday
   Wednesday
   Thursday
   Friday
   Saturday
   Sunday
)

① 使用 iota 定义枚举值

itoa的值从0开始并每行增加1。此版本等同于第一个版本:

  • Monday = 0
  • Tuesday = 1
  • Wednesday = 3
  • 等等

使用 iota 允许我们避免手动定义常量值。例如,在大的枚举中手动设置常量值是会容易出错的。进一步说,我们不用对每一个变量都重复指定Weekday类型:我们定义的所有变量都是一个Weekday类型。

注意:我们可以在更复杂的表达式中使用iota。下面是从Effective Go中出现的一个关于处理ByteSize枚举值的例子:

代码语言:javascript复制
type ByteSize float64
const (
  _ = iota ①
  KB ByteSize = 1 << (10 * iota) ②
  MB ③
  GB
  TB
  PB
  EB
  ZB
  YB
)
① 通过给 _ 赋值忽略第一行的值
② 在该行 iota等于1,因此 KB被设置成 1 << (10 * 1)
③ 在这一行,iota等于2,本行将会重复上一行的表达式,因此 MB 被设置成了 1 << (10 * 2)

Go中Unknow 值的处理

既然我们已经理解了在Go中处理枚举值的原理,让我们考虑下下面的例子。我们将实现一个HTTP处理以便将JSON格式的请求解码成Request结构体类型。该结构体将会包含一个Weekday类型的Unknown值。下面是第一版本的实现:

代码语言:javascript复制
type Weekday int ①

const (
  Monday Weekday = iota
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
  Sunday
  Unknown ②
)

type Request struct { ③
  ID int `json:"id"`
  Weekday Weekday `json:"weekday"`
}

func httpHandler(w http.REsponseWriter, r *http.Request) { ④
  bytes, err != readBody(r) ⑤
  if err != nil {
    w.WriteHeader(http.StatusBadRequest)
    return
  }

  var request Request
  err = json.Unmarshal(bytes, &request) ⑥
  if err != nil {
    w.WriteHeader(http.StatusBadRequest)
    return
  }

  // Use Request
}

① 重用我们定义的 Weekday枚举值

② 定义Unknown常量

③ 定义一个包含Weekday字段的Request结构体

④ 实现一个HTTP处理器

⑤ 读取请求体并返回一个[]byte

⑥ 解码JSON请求体

在这个例子中,我们创建了一个Request结构体,该结构体从一个JSON请求体中解码而来。这段代码非常完整有效。在例子中,我们可以接收一个JSON内容并正确解码:

代码语言:javascript复制
{
  "id": 1234,
  "weekday": 0
}

这里,Weekday字段的值会等于0:Monday。

现在,如果在JSON内容中不包含weekday字段会怎么样呢?

代码语言:javascript复制
{
  "Id": 1235
}

解析该内容的时候将不会引起任何错误。然而,在Request结构体中的Weekday字段值将会被设置成一个int类型的默认值:0值。因此,就像是在上次请求中的Monday。

那我们应该如何区分请求中是传递的Monday还是就没有传递Weekday字段呢?这个问题和我们定义Weekday枚举的方式有关。实际上,Unknown是枚举值的最后一个值。因此,它的值应该等于7.

为了解决该问题,处理一个unknown的枚举值的最好的实践方法是将它设置成0(int类型的零值)。因此,我们应该按如下方式声明Weekday枚举值:

代码语言:javascript复制
type Weekday int

const (
  Unknown Weekday = iota ①
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
  Sunday
)

① Unknow现在等于0了

如果JSON请求体中的weekday的值是空,那将会被解析成 Unknown;这就是我们所需要的。

根据经验,枚举的未知值应该设置为枚举类型的零值。这样,我们就可以区分出显示值和缺失值了

0 人点赞