Golang iota

7.3 用于枚举的 iota

许多编程语言中都提供了枚举类型(Enum),用以表示一组固定的值。Go 语言中没有内置的枚举类型,但提供了 iota,它允许为一组常量赋予递增的整数值。

iota 的最佳用法是首先定义一个基于 int 的新类型,该类型将表示所有有效值。例如:

type MailCategory int    // 基于 int 类型,声明 MailCategory 类型

然后,用 const 关键字为新类型定义一组值:

const (
	Uncategorized MailCategory = iota    // 第一个常量,显式指定类型并赋值为 iota
	Personal                             // 第二个常量,自动继承类型和递增的 iota 值
	Spam                                 // 第三个常量,以此类推
	Social
	Advertisements
)

在 Go 的 const 块中,第一个常量会显式指定类型并赋值为 iota,而后续的常量如果未指定类型和值,编译器会自动复用第一个常量的类型,并依次赋予递增的 iota 值(从 0 开始)。这意味着第一个常量(Uncategorized)被赋值为 0,第二个常量(Personal)被赋值为 1,以此类推。每当遇到一个新的 const 块时,iota 会重置为 0。

const 块中,iota 的值会为每一个常量自动递增,无论该常量是否直接使用了 iota(如下所示):

const (
	Field1 = 0        // 显式赋值为 0,此时 iota = 0(但未使用)
	Field2 = 1 + iota // 显式赋值,此时 iota = 1
	Field3 = 20       // 显式赋值为20,此时 iota = 2(但未使用)
	Field4            // 未赋值,继承上一行的 20,此时 iota = 3
	Field5 = iota     // 显式使用 iota,此时 iota = 4
)

func main() {
	fmt.Println(Field1, Field2, Field3, Field4, Field5)
}

输出结果(出乎意料)为:

0 2 20 20 4
  • Field2 = 2 的原因:iota 在每个 const 块中从 0 开始计数,在第二行(Field2)时,iota 值为 1,表达式 1 + iota 即为 1+1=2
  • Field4 = 20 的原因:const 块中,如果某行(Field4)没有显式赋值,会完全复制上一行(Field3)的值和类型。所以,Field4 自动继承了 Field3 的值和类型,即 20。
  • Field5 = 4 的原因:在 const 块中,iota 计数会为每个常量自动递增(不受中间显式赋值的影响)。即第一行 iota=0(Field1)、iota=1(Field2),以此类推。当遇到 Field5 时,iota 已递增到 4。

const 块中,iota 的行为遵循以下规则:

  • iota 的计数是基于行号的,从 0 开始
  • 即使中间有显式赋值(如 Field3=20),iota 仍然会继续递增
  • 未赋值的常量会自动继承前一个常量的值和类型

iota 的最佳实践

当常量值由外部规范明确规定时,应避免使用 iota,直接赋值即可。例如:实现协议、标准或第三方接口时,应直接赋值(如:const StatusOK = 200)。

iota 应仅用于内部场景——即常量通过名称(而非具体数值)被引用的场合。如此,你既能充分享受 iota 的便利,又能随时在列表中插入新常量,而不用担心破坏现有代码。

——Danny van Heumen

在 Go 语言中,没有任何机制能阻止你(或其他人)为已定义的类型创建额外值。这意味着,如果定义了一个类型,并且类型中用 iota 枚举了一组常量,则你或其他人仍然可以创建这个类型的其他值,即使这些值不在枚举列表中。如果你在一个使用 iota 枚举的常量列表中插入一个新的标识符,所有后续的标识符都会被重新编号。这是因为 iota 枚举是基于常量的位置来生成值的,而不是基于常量的名称。这种重新编号可能会导致问题,特别是当这些常量代表了另一个系统或数据库中的值时。例如,如果你有一个与数据库表中的记录相对应的枚举,插入一个新标识符可能会导致枚举值与数据库中的值不一致,从而破坏应用程序。

鉴于上述两个限制,iota 枚举主要适用于当你只需要区分一组值,而不关心这些值的具体数值的情况。例如,如果你有一组状态值(如“开始”、“进行中”、“完成”),并且你只关心这些状态之间的区别,而不关心它们的具体数值,那么使用 iota 枚举是合适的。如果实际值很重要,你应该明确指定这些值,而不是使用 iota 枚举。例如,如果你有一组与外部系统或数据库中的特定值相对应的常量,你应该直接指定这些值,以确保它们与外部系统或数据库中的值保持一致。

由于可以将字面量表达式赋值给常量,将看到许多示例代码建议在如下这种情况下使用 iota

type BitField int

const (
	Field1 BitField = 1 << iota // assigned 1
	Field2                      // assigned 2
	Field3                      // assigned 4
	Field4                      // assigned 8
)

虽然这很巧妙,但在使用这种编程模式时要小心。如果使用这种模式,请添加足够的注释以描述代码逻辑。如前所述,当关心常量的具体值时,使用 iota 是脆弱的。例如,未来的维护者在列表中间插入一个新的常量,将会破坏应用程序。

请注意,iota 从 0 开始编号。如果使用一组常量来表示不同的配置状态,则零值(Zero Value)可能很有用。例如,前文中的 MailCategory 类型。当邮件首次到达时,它是未分类的,所以零值是有意义的。如果常量没有合理的默认值,一个常见的编程模式是将常量代码块中的第一个 iota 值赋给 _,或者赋给一个表示“值无效”的常量。这样可以轻松检测变量何时未正确初始化。

评论