Go語言有很多工具, goimports用於package的自動導入或者刪除, golint用於檢查源碼中不符合Go coding style的地方, 比如全名,注釋等. 還有其它工具如gorename, guru等工具. 作為工具它們都是使用go語言(查看)開發的, 這些工具都有一個共同點就是: 讀取源代碼, 分析源代碼, 修改或生成新代碼.
簡述
很多編程語言/庫/框架等都能生成代碼, 比如使用rails, 可以輕松地new一個project出來, 生成項目基本代碼, 我們稱其為boilerplate, 或者template, 這已經習以為常了. 像ruby的動態語言通常能在運行時生成代碼, 我們稱之為meta programming(元編程), 比如rails的resources可以生成restful的router出來.因為是運行時動態生成, 因此可能會遇到exception, 以及性能方面有所損失.
像elixir這種編程語言的macro則比ruby的元編程方面向"前"一步, 它在編譯期生成代碼, 而不在運行時生成, 好處是可以生成大量的代碼而對性能幾乎沒有太大影響. 像phoenix框架的router查看部分, 則通過macro生成大量的函數, 利用BEAM的pattern matching機制高效路由.elixir的macro是寫在源代碼里的, 而Go則可以分離.
Go語言可以通過reflect包同樣做到ruby的運行時生成代碼(比如創建對象), 但更強大的一點是, 它通過讀取源碼, 再修改源碼, 生成新的代碼.我們可以將這個過程單獨寫作一個工具, 這個工具可以適用於不同的項目.
例子
package game //go:generate stringer -type=GameStatus // 注意//與go:generate字符之間不能有空格 // GameStatus 表示比賽的狀態 type GameStatus int const ( Unvalid GameStatus = iota ValidFailed Valid Register Start Running End )
運行 go generate 會生成gamestatus_string.go文件, 並且實現了Stringer接口.
同樣的例子在gRPC中也出現過code, 生成的string.正如Rob Pike所說:
let the mechine do the work.source
很多項目在使用數據庫時, 通過tag指定數據庫里的字段名字, 在寫SQL時, 又只能通過字符串來表示字段名, 因此如果某一個字段名修改時, 則意味着涉及到此字段的SQL都面臨着修改, 而我們希望只需要修改一個地方.
有一個結構作為數據庫表結構如下:
type User struct { ID int `json:"id" bson:"id"` Name string `json:"name" bson:"name"` }
當使用這個model里的字段進行sql查詢時, 通常使用:
map[string]interface{}{ "id":123456, }
作為查詢條件, 如果當字段名更改時, 不得不修改這個map里的key值
如果能夠自動生成一個結構體, 用於表示這些column name值, 那么只需修改一處:
map[string]interface{}{ UserColumns.ID: 123456 }
使用方法
gen_columns -tag="bson" -path="./models/user.go"
會生成一個獨立的文件, 里面的內容為:
package models type _UserColumn struct { ID string Name string } var UserColumns _UserColumn func init() { UserColumns.ID = "id" UserColumns.Name = "name" }
總結
gen_columns是自己在項目中遇到問題所給出的解決辦法, 第一版本是通過reflect做的, 總共需要好幾個步驟; 使用ast做就只需在編譯時多加一個go generate, 而這命令基本上可以集成在build的腳本里, 因此不需要再額外擔心代碼生成的問題.
讓我們用Go創造更多生成代碼的工具吧.
其它例子