Go iota 原理和源碼剖析

iota 是 Go 語言的一個保留字,用作常量計數器。由於 iota 具有自增特性,所以可以簡化數字增長的常量定義。

iota 是一個具有魔法的關鍵字,往往令初學者難以理解其原理和使用方法。

本文會從書寫方法、使用場景、實現原理以及優缺點等各方面剖析 iota 關鍵字。 

1. 書寫方法

正確寫法:

const (
  FirstItem = iota
  SecondItem
  ThirdItem
)
// 或者
const SingleItem = iota

錯誤寫法:

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

iota 只能用於常量表達式,而且必須在 const 代碼塊中出現,不允許出現在其它位置。

 

2. 使用場景

iota 的主要使用場景用於枚舉。Go 語言的設計原則追求極盡簡化,所以沒有枚舉類型,沒有 enum關鍵字。

Go 語言通常使用常量定義代替枚舉類型,於是 iota 常常用於其中,用於簡化代碼。

例如:

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 的代碼顯然簡潔優雅很多。不使用 iota 的代碼,對於代碼潔癖者來說,簡直就是一坨,不可接受。

而 Go 語言的發明者,恰恰具有代碼潔癖,而且還是深度潔癖。Go 語言設計初衷之一:追求簡潔優雅。 

3. iota 原理

iota 源碼在 Go 語言代碼庫中,只有一句定義語句,位於內建文件 go/src/builtin/builtin.go 中:

const iota = 0 // Untyped int.

iota 是一個預聲明的標識符,它的值是 0。 在 const 常量聲明中,作為當前 const 代碼塊中的整數序數。

從 Go 語言代碼庫的代碼看,iota 只是一個簡單的整數 0,為什麼能夠作為常量計數器,進行常量自增呢?它的源碼到底在哪裡?

我們做一個小試驗,就會理解其中的道理,看一段代碼:

package main

const (
  FirstItem = iota
  SecondItem
  ThirdItem
)

func main() {
  println(FirstItem)
  println(SecondItem)
  println(ThirdItem)
}

非常簡單,就是打印 FirstItem,SecondItem,ThirdItem。

編譯上述代碼:

go tool compile -N -l main.go

使用 -N -l 編譯參數用于禁止內聯和優化,防止編譯器優化和簡化代碼,弄亂次序。這樣便於閱讀彙編代碼。

導出彙編代碼:

go tool objdump main.o

截取部分結果如下:

TEXT %22%22.main(SB) gofile../Users/wangzebin/test/test/main.go
...
main.go:10    MOVQ $0x0, 0(SP)  // 對應源碼 println(FirstItem)
main.go:10    CALL 0x33b [1:5]R_CALL:runtime.printint
...
main.go:11    MOVQ $0x1, 0(SP)  // 對應源碼 println(SecondItem)
main.go:11    CALL 0x357 [1:5]R_CALL:runtime.printint
...
main.go:11    MOVQ $0x2, 0(SP)  // 對應源碼 println(ThirdItem)
main.go:11    CALL 0x373 [1:5]R_CALL:runtime.printint
...

編譯之後,對應的常量 FirstItem、SecondItem 和 ThirdItem,分別替換為$0x0、$0x1 和 $0x2。

這說明:Go代碼中定義的常量,在編譯時期就會被替換為對應的常量。當然 iota,也不可避免地在編譯時期,按照一定的規則,被替換為對應的常量。

所以,Go 語言源碼庫中是不會有 iota 源碼了,它的魔法在編譯時期就已經施展完畢。也就是說,解釋 iota 的代碼包含在 go 這個命令和其調用的組件中。

如果你要閱讀它的源碼,準確的說,閱讀處理 iota 關鍵字的源碼,需要到 Go 工具源碼庫中尋找,而不是 Go 核心源碼庫。

 

4. iota 規則

使用 iota,雖然可以書寫簡潔優雅的代碼,但對於不熟悉規則的人來講,又帶來的很多不必要的麻煩和誤解。

對於引入 iota,到底好是不好,每個人都有自己的評價。實際上,有些不常用的寫法,甚至有些賣弄編寫技巧的的寫法,並不是設計者的初衷。

大多數情況下,我們還是使用最簡單最明確的寫法,iota 只是提供了一種選擇而已。一個工具使用的好壞,取決於使用它的人,而不是工具本身。

以下是 iota 編譯規則:

1) 依賴 const

iota 依賴於 const 關鍵字,每次新的 const 關鍵字出現時,都會讓 iota 初始化為0。

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

2) 按行計數

iota 按行遞增加 1。

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

3) 多個iota

同一 const 塊出現多個 iota,只會按照行數計數,不會重新計數。

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

與上面的代碼完全等同,b 和 c 的 iota 通常不需要寫。

4) 空行處理

空行在編譯時期首先會被刪除,所以空行不計數。

  const (
    a = iota     // a=0


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

5) 跳值佔位

佔位 “_”,它不是空行,會進行計數,起到跳值作用。

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

6) 開頭插隊

開頭插隊會進行計數。

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

7) 中間插隊

中間插隊會進行計數。

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

8) 一行多個iota

一行多個iota,分別計數。

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

參考資料:

1. go語言編程

2. 編程寶庫