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
)
為了進行除錯,我們希望這些常數能夠列印出它們的美觀格式,這表示我們需要一個方法,簽章為
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 索引的字串切片或地圖,或其他技術。不論我們使用哪個方法,如果我們變更藥丸組,則需要維護該方法,而且需要確認它是正確的。(兩種 paracetamol 名稱比其他方法更加複雜。)再者,採取何種方法的問題取決於類型和值:已簽署或未簽署、密集或分散、以零為基準或其他。
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
印出的方法所使用的做法,會根據常數集的屬性而有所不同。例如,如果常數是分散的,它可能會使用一個對應表。以下是一個以代表二的冪的常數集為基礎的非重要範例
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
generate
的用途已經安裝在 Go 樹狀結構中。範例包括產生 Unicode 表格在unicode
套件中,產生編碼和解碼陣列在encoding/gob
中,產生時區資料在time
套件等等。
請創意地使用go
generate
。它存在那邊是為了鼓勵實驗。
即便你不使用,也要使用新的stringer
工具來為你的整數常數寫String
方法。讓機器來做這件事。
下一篇文章:Gopher Gala 是首個世界性的 Go hackathon
前一篇文章:Go 1.4 已發行
部落格索引