這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
# Go語言AST嘗試
Go語言有很多工具, goimports用於package的自動匯入或者刪除, golint用於檢查源碼中不符合Go coding style的地方, 比如全名,注釋等. 還有其它工具如gorename, guru等工具. 作為工具它們都是使用go語言([查看](https://github.com/golang/tools/tree/master/cmd))開發的, 這些工具都有一個共同點就是: 讀取原始碼, 分析原始碼, 修改或產生新代碼.
## 簡述
很多程式設計語言/庫/架構等都能產生代碼, 比如使用rails, 可以輕鬆地new一個project出來, 產生項目基本代碼, 我們其為boilerplate, 或者template, 這已經習以為常了. 像ruby的動態語言通常能在運行時產生代碼, 我們稱之為meta programming(元編程), 比如rails的resources可以產生restful的router出來.因為是運行時動態產生, 因此可能會遇到exception, 以及效能方面有所損失.
像elixir這種程式設計語言的macro則比ruby的元編程方面向"前"一步, 它在編譯期產生代碼, 而不在運行時產生, 好處是可以產生大量的代碼而對效能幾乎沒有太大影響. 像phoenix架構的router([查看](https://github.com/phoenixframework/phoenix/blob/master/lib/phoenix/router.ex#L375))部分, 則通過macro產生大量的函數, 利用BEAM的高效的pattern matching機制高效路由.elixir的macro是寫在原始碼裡的, 而Go則可以分離.
Go語言可以通過reflect包同樣做到ruby的運行時產生代碼(比如建立對象), 但更強大的一點是, 它通過讀取源碼, 再修改源碼, 產生新的代碼.我們可以將這個過程單獨寫作一個工具, 這個工具可以適用於不同的項目.
## 例子
### [stringer](https://github.com/golang/tools/tree/master/cmd/stringer)
```
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_stirng.go檔案, 並且實現了Stringer介面.
同樣的例子在gRPC中也出現過[code](https://github.com/grpc/grpc-go/blob/master/codes/codes.go#L41), 產生的[string](https://github.com/grpc/grpc-go/blob/master/codes/code_string.go).
正如Rob Pike所說:
> *let the mechine do the work.*
> [source](https://blog.golang.org/generate)
### [gen_columns](https://github.com/qgymje/gen_columns)
很多項目在使用資料庫時, 通過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"
}
```
### 其它例子
* [impl](https://github.com/josharian/impl) 指定一個介面, 產生介面所需方法
* [goa](https://goa.design/) 一套用於寫web service的DSL
### 總結
gen_columns是自己在項目中遇到問題所給出的解決辦法, 第一版本是通過reflect做的, 總共需要好幾個步驟; 使用ast做就只需在編譯時間多加一個go generate, 而這命令基本上可以整合在build的指令碼裡, 因此不需要再額外擔心代碼產生的問題.
讓我們用Go創造更多產生代碼的工具吧.