今后一段時間要研究下go generate,在官網博客上看了Rob Pike寫的generating code,花了一些時間翻譯了下。有幾個句子翻譯的是否正確有待考量,歡迎指正。
生成代碼
通用計算的一個特性--圖靈完備--是一個計算機程序可以編寫一個計算機程序。這是一個強大的想法,盡管經常出現,但還不足夠完美。例如,它是編譯器定義的重要組成部分。它也是go test命令的工作原理:它掃描要測試的軟件包,寫出一個包含為包定制的測試工具的Go程序,然后編譯並運行。現代電腦快到可以在幾分之一秒完成這個看似昂貴的序列。
還有很多程序編寫程序的其他例子。例如,yacc讀入一個語法描述,並寫出一個程序來解析該語法。Protocol buffer“編譯器”讀取接口描述輸出結構定義,方法和其他支持代碼。各種配置工具也是這樣工作的,檢查元數據或環境,輸出自定義的本地配置。
因此,編寫程序的程序是軟件工程的重要組成部分,但是像yacc這些可以生成源代碼的程序需要基礎到構建過程中,以便可以編譯它們的輸出。當使用像Make這樣的外部構建工具時,這通常可以很容易做到。但是在Go中,Go的工具從Go源中獲取所有必要的構建信息,這有一個問題。沒有機制可以單獨地從go tool中運行yacc。
直到現在,就是這樣。
最新的Go發布版,1.4,包含一個新命令,可以更輕松地運行這些工具。它叫做go generate,它可以通過掃描Go源碼中的特殊注釋來識別要運行的常規命令。了解go generate不是go build的一部分很重要。它不包含依賴關系分析,必須在運行go build之前顯式運行。它旨在由Go package的作者使用,而不是其客戶端。
Go generate命令很容易使用。作為一個預熱,下面展示如何使用它來生成yacc語法。假設你有一個名為gopher.y的YACC輸入文件,它定義了一種新語言的語法。要生成實現語法的Go源碼文件,通常會調用Yacc的標准Go版本:
go tool yacc -o gopher.go -p parser gopher.y
-o選項命令輸出文件,-p選項指定包名。
要使go generate驅動這個過程,在同一目錄中的任何一個普通(非生成).go文件中,將該注釋添加的文件中的任何位置:
//go:generate go tool yacc -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 tool,不需要額外安裝,而且很適合Go生態系統。請記住,它是為package作者,而不是客戶端,只是因為它調用的程序在目標機器上可能不可用。另外,如果包含的包是通過go get導入的,一旦文件被生成(並且被測試),他就必須被檢入到源碼庫以供客戶端使用。
現在有了go generate,可以用它來做新的事情。作為一個不同尋常的如何使用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
)
為了調試,我們希望變量很夠很好地打印自己,這意味着我們需要一個具有如下簽名的方法。
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,或者其他一些技術。無論我們做什么,如果我們改變Pills集合,我們需要維護它來保證它是正確的。(Paracetamol的兩個Name比其他的要棘手)。另外,采取哪種方法的問題取決於類型和值:有符號還是無符號,密集還是稀疏,基於零還是不基於零的等等。
Stringer程序負責處理所有這些細節。雖然它可以獨立運行,但是它是由go generate驅動的要使用它,可以向源代碼中添加生成注釋,類型定義附近。
//go:generate stringer -type=Pill
此規則制定go generate 應運行stringer工具以生成Pill類型的String方法。輸出會自動寫入pill_string.go(默認情況下,我們可以使用 -output標志來覆蓋)
運行之后
$ go generate
$ cat pill_string.go
// generated by stringer -type Pill pill.go; DO NOT EDIT
package pill
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生成的Method使用的方法會根據常量集合的屬性而變化。例如,如果常量是稀疏的,它可能會使用一個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)
}
簡而言之,自動生成的method可以做到比人類做地更好。
在go tree中已經安裝了go generate的許多其他用途。包括在unicode包中生產Unicode表,為encoding/gob創建有效的編解碼方法,在time包中創建時區數據等等。
請創造性的使用go generate,鼓勵動手實踐。
即使沒有,使用新的stringer工具為您的整形常量編寫String方法。讓機器做這樣的工作。
By Rob Pike