Go 部落格
生成程式碼
通用計算的一個特性——圖靈完備性——就是計算機程式可以編寫計算機程式。這是一個強大的想法,儘管它經常發生,但並沒有得到應有的重視。例如,它是編譯器定義的重要組成部分。go
test
命令也是這樣工作的:它掃描要測試的包,寫出一個包含為該包定製的測試套件的 Go 程式,然後編譯並執行它。現代計算機非常快,這段聽起來很費時的序列可以在幾分之一秒內完成。
還有很多其他程式編寫程式的例子。例如,Yacc 讀取語法描述並寫出一個程式來解析該語法。協議緩衝區“編譯器”讀取介面描述並輸出結構定義、方法和其他支援程式碼。各種配置工具也都是這樣工作的,檢查元資料或環境並輸出根據本地狀態定製的腳手架。
因此,編寫程式的程式是軟體工程中的重要元素,但像 Yacc 這樣生成原始碼的程式需要整合到構建過程中,以便其輸出可以被編譯。當使用像 Make 這樣的外部構建工具時,這通常很容易做到。但在 Go 中,其 go 工具從 Go 原始碼獲取所有必要的構建資訊,這裡有一個問題。僅靠 go 工具根本沒有機制來執行 Yacc。
也就是說,直到現在。
最新的 Go 釋出版本 1.4 包含一個新的命令,可以更輕鬆地執行此類工具。它叫做 go
generate
,它的工作原理是掃描 Go 原始碼中用於標識要執行的通用命令的特殊註釋。重要的是要理解 go
generate
不是 go
build
的一部分。它不包含任何依賴分析,並且必須在執行 go
build
之前顯式執行。它旨在供 Go 包的作者使用,而不是其客戶端。
go
generate
命令易於使用。作為熱身,下面介紹如何使用它來生成 Yacc 語法。
首先,安裝 Go 的 Yacc 工具
go get golang.org/x/tools/cmd/goyacc
假設您有一個名為 gopher.y
的 Yacc 輸入檔案,它定義了您新語言的語法。要生成實現該語法的 Go 原始檔,您通常會像這樣呼叫命令
goyacc -o gopher.go -p parser gopher.y
-o
選項指定輸出檔名,而 -p
指定包名。
要讓 go
generate
驅動該過程,在同一目錄中的任何常規(非生成).go
檔案中,在該檔案中的任何位置新增此註釋
//go:generate goyacc -o gopher.go -p parser gopher.y
此文字只是上面命令的前面加上了一個由 go
generate
識別的特殊註釋。註釋必須以行首開頭,並且 //
和 go:generate
之間不能有空格。在該標記之後,行的其餘部分指定了 go
generate
要執行的命令。
現在執行它。切換到源目錄並執行 go
generate
,然後是 go
build
等等
$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test
就是這樣。假設沒有錯誤,go
generate
命令將呼叫 yacc
建立 gopher.go
,此時該目錄包含全套 Go 原始檔,因此我們可以正常構建、測試和工作。每次修改 gopher.y
時,只需重新執行 go
generate
即可重新生成解析器。
有關 go
generate
工作原理的更多詳細資訊,包括選項、環境變數等,請參閱設計文件。
Go generate 所做的一切都可以透過 Make 或其他構建機制完成,但它是隨 go
工具一起提供的——無需額外安裝——並且很好地融入了 Go 生態系統。請記住,它是用於包作者,而不是客戶端,僅僅是因為它呼叫的程式可能在目標機器上不可用。此外,如果包含的包旨在透過 go
get
匯入,則檔案生成(並測試!)後,必須將其檢入原始碼倉庫以供客戶端使用。
既然我們有了它,讓我們用它做一些新的事情。作為一個截然不同的例子,說明 go
generate
如何提供幫助,golang.org/x/tools
倉庫中有一個新程式叫做 stringer
。它不是釋出版本的一部分,但很容易安裝
$ go get golang.org/x/tools/cmd/stringer
這裡有一個來自 stringer
文件的例子。假設我們有一些程式碼包含一組定義不同型別藥丸的整數常量
package painkiller
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
為了除錯,我們希望這些常量能夠自如地美觀列印,這意味著我們需要一個具有如下簽名的“String”方法:
func (p Pill) String() string
手工編寫一個很容易,也許像這樣
func (p Pill) String() string {
switch p {
case Placebo:
return "Placebo"
case Aspirin:
return "Aspirin"
case Ibuprofen:
return "Ibuprofen"
case Paracetamol: // == Acetaminophen
return "Paracetamol"
}
return fmt.Sprintf("Pill(%d)", p)
}
當然,還有其他方法來編寫此函式。我們可以使用按 Pill 索引的字串切片,或者使用 map,或者其他一些技術。無論我們做什麼,如果改變藥丸的集合,我們就需要維護它,並且需要確保它是正確的。(撲熱息痛的兩個名字使得這比原本更棘手。)此外,採取哪種方法的問題本身取決於型別和值:有符號或無符號、密集或稀疏、從零開始或非從零開始,等等。
stringer
程式負責處理所有這些細節。儘管它可以獨立執行,但它旨在由 go
generate
驅動。要使用它,請在原始碼中新增一個生成註釋,可能靠近型別定義處
//go:generate stringer -type=Pill
此規則指定 go
generate
應執行 stringer
工具為型別 Pill
生成一個 String
方法。輸出會自動寫入到 pill_string.go
檔案(這是一個可以透過 -output
標誌覆蓋的預設設定)。
讓我們執行它
$ go generate
$ cat pill_string.go
// Code generated by stringer -type Pill pill.go; DO NOT EDIT.
package painkiller
import "fmt"
const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"
var _Pill_index = [...]uint8{0, 7, 14, 23, 34}
func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}
$
每當我們更改 Pill
的定義或常量時,我們所需要做的就是執行
$ go generate
以更新 String
方法。當然,如果我們在同一個包中以這種方式設定了多個型別,只需一個命令就可以更新它們所有的 String
方法。
毫無疑問,生成的方法很醜陋。不過這沒關係,因為人類不需要處理它;機器生成的程式碼通常都很醜陋。它在努力追求效率。所有名稱都被壓縮到一個單獨的字串中,這節省了記憶體(所有名稱只有一個字串頭,即使有數百萬個)。然後透過一種簡單高效的技術,一個數組 _Pill_index
將值對映到名稱。還要注意,_Pill_index
是一個 uint8
型別的陣列(而不是切片;又省去一個頭),它是足以覆蓋所有值的最小整數型別。如果值更多,或者有負值,生成的 _Pill_index
型別可能會變為 uint16
或 int8
:哪種最合適就用哪種。
stringer
列印的方法所使用的方法取決於常量集的屬性。例如,如果常量是稀疏的,它可能會使用 map。這裡有一個基於表示二的冪的常量集的簡單例子
const _Power_name = "p0p1p2p3p4p5..."
var _Power_map = map[Power]string{
1: _Power_name[0:2],
2: _Power_name[2:4],
4: _Power_name[4:6],
8: _Power_name[6:8],
16: _Power_name[8:10],
32: _Power_name[10:12],
...,
}
func (i Power) String() string {
if str, ok := _Power_map[i]; ok {
return str
}
return fmt.Sprintf("Power(%d)", i)
}
簡而言之,自動生成方法使我們能夠比預期的人工操作做得更好。
Go 原始碼樹中已經安裝了許多 go
generate
的其他用途。例如,在 unicode
包中生成 Unicode 表,在 encoding/gob
中建立高效的陣列編碼和解碼方法,在 time
包中生成時區資料,等等。
請創造性地使用 go
generate
。它旨在鼓勵實驗。
即使您不這樣做,也請使用新的 stringer
工具為您的整數常量編寫 String
方法。讓機器來完成這項工作。
下一篇文章:Gopher Gala 是首個全球性 Go 駭客馬拉松
上一篇文章:Go 1.4 釋出了
部落格索引