Go常见错误集锦 | 如何正确设置枚举值中的零值

589 阅读5分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

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

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

type Weekday intconst (
    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 intconst (
	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;这就是我们所需要的。

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