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

yudotyang · 2021年07月28日 · 最后由 astaxie 回复于 2021年07月29日 · 286 次阅读
本帖已被设为精华帖!

本文是对《100 Go Mistackes:How to Avoid Them》一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。 本文首发于微信公众号 “Go 学堂”,想获取更多信息,请关注该公众号。

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

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

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 类型,那么下面的函数签名对于调用者来说可能会有一点模糊:

func GetCurrentWeekday() int {
    // ...
}

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

func getCurrentWeekday() Weekday {
    // ...
}

在这个例子中,我们强制指定了返回具体的类型。

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

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

iota

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

下面是用 iota 的 Weekday 版本:

type Weekday int
const (
    Monday Weekday = iota 
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

① 使用 iota 定义枚举值

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

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

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

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

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 的枚举中如何处理未知值(unknown values)

Unknow 值

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

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 内容并正确解码:

{
    "id": 1234,
    "weekday": 0
}

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

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

{
    "Id": 1235
}

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

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

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

type Weekday int

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

① Unknow 现在等于 0 了

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

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

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 07月29日 14:21
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册