Golang iota 用法解析

2,625 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

开篇

我们在 Golang 开发中经常看到 iota 在 const 中出现,它可以说是 Golang 用来弥补没有 enum 枚举类型的补充,本质是针对一系列可枚举的常量而设计的语法糖。

根据笔者日常经验,其实 iota 如果在不了解原理的情况下直接用,还是容易有一些混淆的时刻,对于常量具体的值,其实是不太有信心的。因为 iota 的用法比较独树一帜,本身有很多约定。

这篇文章,希望能够帮你梳理清楚 iota 的规范,原理。以后碰到 iota 不用再去纠结。内容不多,记住就ok。

下面是一个最简单的例子,我们不在意具体的枚举值,只是需要几个常量定义即可,这个时候用 iota 是最简洁的。

// old
const (
    LocationShady    = 0
    LocationWalnut   = 1
    LocationKenmawr  = 2
)

// new
const (
    LocationShady    = iota  //0
    LocationWalnut           //1
    LocationKenmawr          //2
)

官方spec

首先我们还是先看下 Golang 官方 spec 对于 iota 的说明:go.dev/ref/spec#Io…

Within a constant declaration, the predeclared identifier iota represents successive untyped integer constants. Its value is the index of the respective ConstSpec in that constant declaration, starting at zero. It can be used to construct a set of related constants.

提炼要点:

  • iota 是一个 Golang 预先声明的标识符;
  • 只能在 const 声明中存在;
  • iota 代表了一组连续的,无类型的,整数常量;
  • iota 的值由对应 const 声明中的 index 下标决定(后续会详细说明),从0开始。
  • iota 可以被用来构建一组互相关联的常量。

我们来看看官方的示例,感受一下 iota 取值的变化,这里我补上了注释,可以参考:

// 同一个 const 声明块里,连续的多个 iota 的效果跟只有第一行 c0 = iota 是一样的,每行累加1, 默认值为0
const (
	c0 = iota  // c0 == 0
	c1 = iota  // c1 == 1
	c2 = iota  // c2 == 2
)


// 无论这一行有没有出现 iota,计数器都会继续加
const (
	a = 1 << iota  // a == 1  (iota == 0)
	b = 1 << iota  // b == 2  (iota == 1)
	c = 3          // c == 3  (iota == 2, unused)
	d = 1 << iota  // d == 8  (iota == 3)
)

// iota 是无类型的,每行定义的值,可以使用 iota 与表达式配合进行计算
const (
	u         = iota * 42  // u == 0     (untyped integer constant)
	v float64 = iota * 42  // v == 42.0  (float64 constant)
	w         = iota * 42  // w == 84    (untyped integer constant)
)

// 两个 const 块之间的 iota 不会互相影响
const x = iota  // x == 0
const y = iota  // y == 0

利用 iota 逐行递增的特性,我们还可以自行控制逻辑,生成所需的常量组

//偶数常量组
const (
    even1    = (iota+1)*2  //2
    even2                  //4
    even3                  //6
    even4                  //8
    even5                  //10
)

注意,iota 只能用于常量表达式,而且必须在 const 代码块中出现,不允许出现在其它位置。 类似下面这样的代码是无法通过编译的:

var FirstItem = iota
// 或者
println(iota)iota 

定位

  • iota是希腊字母,样子类似拉丁字母“i”,用作数学符号时,代表一个顺序迭代序号。
  • Golang 的 iota 是一个单纯的语言层面上的“元编特性”,用起来是一种代码生成规则。
  • Golang 的 iota 不是函数,不可以在运行时使用,只能在编译时展开,最终表现为一段生成的代码,iota表达式在这段代码中被计算并替换为常数。

优势

  1. 解耦 常量,应对频繁的代码修改更有优势。同一列枚举经过多次修改后,序号会非常混乱了,iota帮你自动维护之后就不关心了

  2. 参与常量计算,不仅仅可以是序号,类似 bitwise flags 这样的场景也能维护。 一个典型的使用案例:计算各个单位的字节size:

package main

const (
  B  = 1 << (10 * iota) // 1 << (10*0)
  KB                    // 1 << (10*1)
  MB                    // 1 << (10*2)
  GB                    // 1 << (10*3)
  TB                    // 1 << (10*4)
  PB                    // 1 << (10*5)
  EB                    // 1 << (10*6)
  ZB                    // 7 << (10*5)
)

func main() {
  println(B, KB, MB, GB, TB)
}
// 输出结果: 1 1024 1048576 1073741824

而不是下面这样:


const (
    B  = 1
    KB = 1024
    MB = 1048576
    GB = 1073741824
    ...
)

实现原理

iota 源码在 Go 语言代码库中,只有一句定义语句,位于内建文件 go/src/builtin/builtin.go 中:

// iota is a predeclared identifier representing the untyped integer ordinal
// number of the current const specification in a (usually parenthesized)
// const declaration. It is zero-indexed.
const iota = 0 // Untyped int.
  • 预声明的标识符(predeclared identifier)
  • 作为当前 const 代码块中的整数序数 (untyped integer ordinal number in a const declaration )
  • 从0开始 (zero-indexed)

编译规则

iota 会在编译时期,按照一定的规则,被替换为对应的常量。所以,Go 语言源码库中是不会有 iota 源码了,它的魔法在编译时期就已经施展完毕。也就是说,解释 iota 的代码包含在 go 这个命令和其调用的组件中。

下面我们看一下 iota 的编译规则:

依赖 const,初始化为 0

iota 依赖于 const 关键字,每次新的 const 关键字出现时,都会让 iota 初始化为0。

const a = iota // a=0
const (
  b = iota     // b=0
  c            // c=1
)

按行计数

iota 按行递增加 1const (
  a = iota     // a=0
  b            // b=1
  c            // c=2
)

多个iota不会重新计数

同一 const 块出现多个 iota,只会按照行数计数,不会重新计数。

const (
    a = iota     // a=0
    b = iota     // b=1
    c = iota     // c=2
  )

与上面的代码完全等同,b 和 c 的 iota 通常不需要写。

空行不计数

空行在编译时期首先会被删除,所以空行不计数。

const (
    a = iota     // a=0


    b            // b=1
    c            // c=2
  )

跳值占位计数

占位 "_",它不是空行,会进行计数,起到跳值作用。

const (
    a = iota     // a=0
    _            // _=1
    c            // c=2
  )

开头插队已经开始计数

开头插队会进行计数,并不是从第一个 iota 出现的位置

const (
    i = 3.14 // i=3.14
    j = iota // j=1
    k = iota // k=2
    l        // l=3
)

中间插队不影响

中间插队会进行计数,把 j = 3.14 替换成 iota 也是一样的。

const (
    i = iota // i=0
    j = 3.14 // j=3.14
    k = iota // k=2
    l        // l=3
)

一行多个iota 分别计数

const (
    i, j = iota, iota // i=0,j=0
    k, l              // k=1,l=1
)

参考资料

4 iota enum examples

Go iota 原理和源码剖析

Where and When to use Iota in Go