Go工具之generate

Go語言提供了一系列強大的工具,靈活使用這些工具,能夠讓我們的項目開發更加容易,工具集包含如下。

bug         start a bug report
build       compile packages and dependencies
clean       remove object files and cached files
doc         show documentation for package or symbol
env         print Go environment information
fix         update packages to use new APIs
fmt         gofmt (reformat) package sources
generate    generate Go files by processing source
get         add dependencies to current module and install them
install     compile and install packages and dependencies
list        list packages or modules
mod         module maintenance
run         compile and run Go program
test        test packages
tool        run specified go tool
version     print Go version
vet         report likely mistakes in packages

工具的源碼位於$GOPATH/src/cmd/internal,本篇文章主要討論Go工具generate。

go語言自動化工具

go generate常用於自動生成代碼,它可以在代碼編譯之前根據源代碼生成代碼。當運行go generate時,它將掃描與當前包相關的源代碼文件,找出所有包含"// go:generate"的註釋語句,提取並執行該註釋後的命令,命令爲可執行程序。該過程類似於調用執行shell腳本。

使用方法

  • 添加特殊註釋
//go:generate command argument...
  • 執行generate命令
$ go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]

注意事項

  • 該特殊註釋必須包含在.go源碼文件中。
  • 每個源碼文件可以包含多個generate特殊註釋。
  • go generate不會被類似go build,go get,go test等命令觸發執行,必須由開發者顯式使用。
  • 命令執行是串行的,如果出錯,後續命令不再執行。
  • 特殊註釋必須以“//go:generate”開頭,雙斜線之後沒有空格。
  • 執行命令必須是系統PATH(echo $PATH)下的可執行程序。

使用示例

package main

import "fmt"

//go:generate echo GoGoGo!
//go:generate go run main.go
//go:generate echo $GOARCH $GOOS $GOFILE $GOLINE $GOPACKAGE

func main() {
 fmt.Println("go rum main.go!")
}

執行go generate命令

$ go generate
GoGoGo!
go rum main.go!
amd64 darwin main.go 7 main

爲枚舉常量實現String方法

看完上述generate的簡單介紹,可能讀者並沒有感受到該工具的強大之處,小菜刀提供一個該工具的經典應用場景:爲枚舉常量實現String方法。

這裏需要提及官方的另外一個工具stringer,它可以自動爲整數常量集編寫String()方法。由於stringer並不在Go官方發行版的工具集裏,我們需要自行安裝,執行如下命令。

go get golang.org/x/tools/cmd/stringer

這裏引用stringer文檔中的一個示例。代碼如下,其定義了一組不同Pill類型的整數常量。

package painkiller

type Pill int

const (
    Placebo Pill = iota
    Aspirin
    Ibuprofen
    Paracetamol
    Acetaminophen = Paracetamol
)

爲了進行調試或者其他原因,我們希望這些常量能夠打印出來,這意味着Pill要有一個帶有簽名的方法。

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名單裏新增了一批藥品名,每次增加或修改藥品名,在相應的簽名函數裏,也都需要進行更改。這樣豈不是很麻煩且很可能遺漏或出錯?這時,我們可以通過 go generate + stringer的方案解決該問題。很簡單,只需在定義Pill的代碼中,增加一句註釋語句即可。

//go:generate stringer -type=Pill

上面的命令,代表運行stringer工具來爲Pill類型生成String方法,默認輸出到pill_string.go文件中,執行如下。

$ 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

當然,你要是覺得這樣麻煩,或者擔心忘記執行generate語句。那麼,可以將go generate語句寫入Makefile之中,置於go build命令之前,實現代碼生成與編譯的自動化。

值得一提的是,在Go源碼文檔中,大量採用了go generate+stringer的方案實現對枚舉常量的String方法。在小菜刀本機Go 1.14.1的源碼下,一共有23處使用,具體如下。

在這裏插入圖片描述

總結

本文主要介紹generate是什麼,能做什麼,如果想深入理解其內在實現邏輯,可以去看Go源碼中生成代碼的詳細過程,例如sort包下通過genzfunc.go實現zfuncversion.go的生成。在Go源碼寶庫中,可以找到很多相似的實現邏輯,參照如下。
在這裏插入圖片描述
它們利用Go編譯器提供的庫,包括定義抽象語法樹的 go/ast、解析抽象語法樹的go/parser、解析用於格式化代碼的 go/format、用於Go詞法標記的go/token等。解析源文件並按照已有的模板生成新的代碼,這一過程和Web 服務中利用模板生成 HTML 文件類似。

總結:減少代碼的重複編寫,保護頭髮!!

參考

https://golang.org/cmd/go/
https://blog.golang.org/generate
https://godoc.org/golang.org/x/tools/cmd/stringer
https://docs.google.com/document/d/1V03LUfjSADDooDMhe-_K59EgpTEm3V8uvQRuNMAEnjg/edit#
https://mp.weixin.qq.com/s/vz-Qpt9c5WhyynpBrJLmGA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章