带你开始探究 Go iota

我为什么会探究 iota 呢?

这就要从我最近遇到的一次 protobuf 解析报错说起,错误信息如下:

  1. cannot parse reserved wire type

所以有关这个错误的原因,可以查看👇这篇文章

https://stackoverflow.com/questions/62033409/cannot-find-solutions-to-a-protobuf-unmarshal-error

在查看这个错误的源码定义时,发现它们是一组 const iota 定义,而 const 值是通过 -iota 来赋值的,具体代码:protobuf/encoding/protowire/wire.go

from: google.golang.org/protobuf/encoding/protowire/wire.go

  1. const (
  2. _ = -iota
  3. errCodeTruncated
  4. errCodeFieldNumber
  5. errCodeOverflow
  6. errCodeReserved
  7. errCodeEndGroup
  8. )

说实话,我用 Go 这么久,还是第一次见 -iota,所以我就一下子来了兴趣,特别想要了解它,首先想到的就是去查 go ref/spec https://golang.org/ref/spec#Iota,但是文中并没有提到 -iota 的用法。

为了了解清楚 iota ,所以我就开始探究,并将我探究的过程记录下来,抛砖引玉,希望可以跟大家一起探讨。

iota 由来

Iota (大写 I 小写 l,中文音译:约塔),是一个全名,而不是某一组词的缩写。iota 是希腊字母中的第 9 个字母。

带你开始探究 Go iota - 图1

希腊字母 iota 的词源(Etymology)

Iota From Ancient Greek ἰῶτα (iôta). (jot): In reference to a phrase in the New Testament: “until heaven and earth pass away, not an iota, not a dot, will pass from the Law” (Mt 5:18), iota being the smallest letter of the Greek alphabet.

维基百科

网上还有以下的阐述(但是我并不了解,所以无法考证,保留至此,以供大家参考)

iota 是典型的数学符号,它所表示的含义如下:1. 作为求和算法的迭代器 2. 作为下标索引 3. 对于复数的虚部

希腊字母 iota 在编程语言 APL 中用于生成连续整数序列(可以参考阅读 iota Scheme 文献)。

APL 是 A Programming Language 或 Array Processing Language 的缩写。肯尼斯・艾佛森在 1962 年设计这个语言时他正在哈佛大学工作,1979 年他因对数学表达式和编程语言理论的贡献而得到图灵奖。在过去数十年的使用历史中,APL 从它的原始版本开始不断改变和发展,今天的版本与 1963 年发表时的版本已经非常不一样了。但它始终是一种解释执行的计算机语言。

维基百科

在 C++ 及其他语言中也有类似于 Go iota 的用法。

Go iota 究竟有何用处

可以被当做 enum 来使用,在 const 块中,默认值为 0,即第一行为 0,以后每一行加 1;

Go 中 iota 的使用大致是基于 APL 中的定义来实现的。

https://en.wikipedia.org/wiki/Iota

iota 的用法 / 注意事项 / 探究

  1. 不同 const 定义块互不干扰
  2. 所有注释行和空行全部忽略
  3. 从第 1 行开始,iota 从 0 逐行加 1

接下来我们来看一个 Go issues 上提到的一些有关 iota 使用的例子

https://github.com/golang/go/issues/39751

  1. type myConst int
  2. const (
  3. zero myConst = iota // iota = 0
  4. one // iota = 1
  5. three = iota + 1 // iota = 2
  6. foure // iota = 3
  7. five = iota + 1 // iota = 4
  8. )
  9. func testIota() {
  10. // 3 4 5 why not 3 4 6
  11. fmt.Println(three, foure, five)
  12. }

我们知道 iota 的值是从第一行 0 ,开始逐行递增的,对应 iota + 1 只是普通的表达式计算,对于 iota 来说是没有影响的。

我们也可以通过修改源代码,然后再编译为新的 go 来执行我们的程序,修改代码块为:src/cmd/compile/internal/noder/noder.go#L456 constDecl() 在 cs.iota++ 之前打印,得到以下值,也印证了我上面说的话。

  1. constState.iota: 0

constState.iota: 1

  1. constState.iota: 2
  2. constState.iota: 3
  3. constState.iota: 4

iota 能不能被用于普通变量申明?

Robert Griesemer 在 2017 年 8 月 16 日提了一个 proposal 想要在 Go2 做这件事。

https://github.com/golang/go/issues/21473

-iota 是什么❓

这个用法我们可以直接查看 Go 标准包,src/text/scanner/scanner.go:

  1. // The result of Scan is one of these tokens or a Unicode character.
  2. const (
  3. EOF = -(iota + 1)
  4. Ident
  5. Int
  6. Float
  7. Char
  8. String
  9. RawString
  10. Comment
  11. // internal use only
  12. skipComment
  13. )

如果你不通过测试,你知道 Ident 的值为 -2 吗?

还有 src/cmd/asm/internal/arch/arch.go:

  1. // Pseudo-registers whose names are the constant name without the leading R.
  2. const (
  3. RFP = -(iota + 1)
  4. RSB
  5. RSP
  6. RPC
  7. )

以及 src/cmd/compile/internal/gc/bexport.go

  1. // Tags. Must be < 0.
  2. const (
  3. // Objects
  4. packageTag = -(iota + 1)
  5. constTag
  6. typeTag
  7. varTag
  8. funcTag
  9. endTag
  10. // Types
  11. namedTag
  12. arrayTag
  13. sliceTag
  14. dddTag
  15. structTag
  16. pointerTag
  17. signatureTag
  18. interfaceTag
  19. mapTag
  20. chanTag
  21. // Values
  22. falseTag
  23. trueTag
  24. int64Tag
  25. floatTag
  26. fractionTag // not used by gc
  27. complexTag
  28. stringTag
  29. nilTag
  30. unknownTag // not used by gc (only appears in packages with errors)
  31. // Type aliases
  32. aliasTag
  33. )

-iota 是对 iota 增加了一个负号,算是一个表达式,所以对应的结果仅仅是做了一个负值计算。

iota 源码级探究

非常感谢欧神对此处源码阅读的指导,大家如果对 Go 源码感兴趣,可以直接读欧神写的书📚《Go 语言原本》

在线阅读地址:https://golang.design/under-the-hood/

为什么 -iota 之后的 const 值是在递减呢?

https://golang.design/gossa?id=b49e9104-3750-4a47-ba55-e491489d8cf1

带你开始探究 Go iota - 图2

src/cmd/compile/internal/ir 下面有一个 ConstExpr

  1. type ConstExpr struct {
  2. miniExpr
  3. origNode
  4. val constant.Value
  5. }

注意:此处的 ConstExpr 是在 Go 仓库 master 上才有,在当前 go1.16.3 是没有这个定义的。

额外补充有关 ir 重构相关的内容

为什么要重构一个 ir 出来,我们可以根据 git commit history 追溯得知

如果要完全分解 gc 包,则需要将其定义的编译器 IR 移到一个单独的包中,该包可以由 gc 本身导入的包导入。

为什么要重构为 ConstExpr,我们可以在 CL 上看到描述:

言归正传,我们继续来看 iota 是如何计算的。

最容易想到的定位方法,就是全局检索: iota 然后查看其赋值即可,但是现实是远没有我们想象的那么简单。。。

  1. // constState tracks state between constant specifiers within a
  2. // declaration group. This state is kept separate from noder so nested
  3. // constant declarations are handled correctly (e.g., issue 15550).
  4. type constState struct {
  5. group *syntax.Group
  6. typ ir.Ntype
  7. values []ir.Node
  8. iota int64
  9. }

入口函数 src/cmd/compile/main.go,

  1. func main() {
  2. gc.Main(archInit)
  3. }

gc.Main: src/cmd/compile/internal/gc/main.go :

  1. func Main(archInit func(*ssagen.ArchInfo)) {
  2. ...
  3. // Parse and typecheck input.
  4. noder.LoadPackage(flag.Args())
  5. ...
  6. }

LoadPackage 的伪代码:

  1. func LoadPackage(filenames []string) {
  2. ...
  3. for _, p := range noders {
  4. p.node()
  5. p.file = nil // release memory
  6. }
  7. ...
  8. // Process top-level declarations in phases.
  9. // Phase 1: const, type, and names and types of funcs.
  10. // This will gather all the information about types
  11. // and methods but doesn't depend on any of it.
  12. //
  13. // We also defer type alias declarations until phase 2
  14. // to avoid cycles like #18640.
  15. // TODO(gri) Remove this again once we have a fix for #25838.
  16. ...
  17. // Phase 2: Variable assignments.
  18. // To check interface assignments, depends on phase 1.
  19. ...
  20. // Phase 3: Type check function bodies.
  21. ...
  22. // Phase 4: Check external declarations.
  23. // TODO(mdempsky): This should be handled when type checking their
  24. // corresponding ODCL nodes.
  25. ...
  26. // Phase 5: With all user code type-checked, it's now safe to verify map keys.
  27. // With all user code typechecked, it's now safe to verify unused dot imports.
  28. }

在 Process top-level declarations in phases 之前的 p.node () 主要就干了一件事:

  1. typecheck.Target.Decls = append(typecheck.Target.Decls, p.decls(p.file.DeclList)...)

对于 const 来说, decls 就是执行 func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node

  1. func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
  2. ...
  3. n.SetIota(cs.iota)
  4. ...
  5. cs.iota++
  6. return nn
  7. }

从而得到了我们的 iota 为一个递增的值。

而 typecheck 在 go 1.16.3 是在以上几个阶段的时候实时执行的,而在 master 上,已经不是这样实现了。

  1. func typecheck(n ir.Node, top int) (res ir.Node) {
  2. ...
  3. // Resolve definition of name and value of iota lazily.
  4. n = Resolve(n)
  5. ...
  6. n = typecheck1(n, top)
  7. ...
  8. if t != nil {
  9. n = EvalConst(n)
  10. t = n.Type()
  11. }
  12. ...
  13. return n
  14. }

我们拆解以上逻辑块:

  1. Resolve 得到 iota 的值,注意这里的值其实是正值
  1. // Resolve ONONAME to definition, if any.
  2. func Resolve(n ir.Node) (res ir.Node) {
  3. ...
  4. if r.Op() == ir.OIOTA {
  5. if x := getIotaValue(); x >= 0 {
  6. return ir.NewInt(x)
  7. }
  8. return n
  9. }
  10. ...
  11. }

getIotaValue() 是从 typecheckdefstack 中取最后一个 ir.Name,然后取 Offset_ 的值。

  1. // type checks the whole tree of an expression.
  2. // calculates expression types.
  3. // evaluates compile time constants.
  4. // marks variables that escape the local frame.
  5. // rewrites n.Op to be more specific in some cases.
  6. var typecheckdefstack []*ir.Name
  7. ...
  8. // getIotaValue returns the current value for "iota",
  9. // or -1 if not within a ConstSpec.
  10. func getIotaValue() int64 {
  11. if i := len(typecheckdefstack); i > 0 {
  12. if x := typecheckdefstack[i-1]; x.Op() == ir.OLITERAL {
  13. return x.Iota()
  14. }
  15. }
  16. }

ir.Name 中的 Offset_ 的相关操作:

  1. func (n *Name) Iota() int64 { return n.Offset_ }
  2. func (n *Name) SetIota(x int64) { n.Offset_ = x }
  1. typecheck1 进行了一系列的猛操作就是为 EvalConst 铺路。
  2. EvalConst 逻辑块:

https://github.com/golang/go/blob/2ebe77a2fda1ee9ff6fd9a3e08933ad1ebaea039/src/cmd/compile/internal/typecheck/const.go#L395

  1. // EvalConst returns a constant-evaluated expression equivalent to n.
  2. // If n is not a constant, EvalConst returns n.
  3. // Otherwise, EvalConst returns a new OLITERAL with the same value as n,
  4. // and with .Orig pointing back to n.
  5. func EvalConst(n ir.Node) ir.Node {
  6. // Pick off just the opcodes that can be constant evaluated.
  7. switch n.Op() {
  8. // 而构建 Const 的关键就是 tokenForOp[n.Op()],到底是 + 还是 -
  9. return OrigConst(...)
  10. }
  11. ...
  12. }

而 constant.UnaryOp 的代码就是根据 token.SUB (-) 计算值的:

  1. // UnaryOp returns the result of the unary expression op y.
  2. // The operation must be defined for the operand.
  3. // If prec > 0 it specifies the ^ (xor) result size in bits.
  4. // If y is Unknown, the result is Unknown.
  5. //
  6. func UnaryOp(op token.Token, y Value, prec uint) Value {
  7. ...
  8. case token.SUB:
  9. switch y := y.(type) {
  10. case unknownVal:
  11. return y
  12. case int64Val:
  13. if z := -y; z != y {
  14. return z // no overflow
  15. }
  16. return makeInt(newInt().Neg(big.NewInt(int64(y))))
  17. case intVal:
  18. return makeInt(newInt().Neg(y.val))
  19. case ratVal:
  20. return makeRat(newRat().Neg(y.val))
  21. case floatVal:
  22. return makeFloat(newFloat().Neg(y.val))
  23. case complexVal:
  24. re := UnaryOp(token.SUB, y.re, 0)
  25. im := UnaryOp(token.SUB, y.im, 0)
  26. return makeComplex(re, im)
  27. }
  28. ...
  29. }

终上所述,我们就大概了解其 Go iota 的递增、递减过程了。

有关 go 支持 enum 的说明和 issues

https://github.com/golang/go/issues/28987#issuecomment-497108307

参考资料

文中所涉及的 CL:

  1. https://go-review.googlesource.com/c/go/+/273008
  2. https://go-review.googlesource.com/c/go/+/275033