每個(gè) gopher 都需要了解的 Go AST
最近業(yè)務(wù)遷移,大約 100+ 個(gè)接口需要從舊的服務(wù),遷到公司框架。遇到幾個(gè)痛點(diǎn):
結(jié)構(gòu)體 dto 做 diff, 對(duì)比結(jié)果 自定義的結(jié)構(gòu)體與 protobuf 生成的互相轉(zhuǎn)換,基于 json tag
這類工作要么手寫(編譯期), 要么 reflect 反射實(shí)現(xiàn)(運(yùn)行時(shí))。其中 #1 考濾到性能問題,手寫最優(yōu),但是結(jié)構(gòu)體太大,同時(shí) 100+ 個(gè)接口遷移,工作量可以想象
google 開源的 go-cmp[1], 輸出美觀,反射性能開銷大了點(diǎn)。當(dāng)前業(yè)務(wù)大量使用,堆機(jī)器吧又不是不能用
#2 目前不好解決,可以簡單的 json Marshal 再 Unmarshal, 但有些字段類型不一致,同時(shí)如何做 json tag 到 pb tag 轉(zhuǎn)換呢?
我們當(dāng)前的方案是通過解析 ast, 讀源碼生成結(jié)構(gòu)體樹,然后 BFS 遍歷自動(dòng)生成轉(zhuǎn)換代碼
//go:generate ast-tools --action convert --target-pkg aaa/dto/geresponse --src-pkg bbb/dto --source aaaResponse ?--target bbbResponse
結(jié)合 go generate 自動(dòng)生成,這是我們的目標(biāo)
Go AST 基礎(chǔ)
不搞編譯器的大多只需要懂前端,不涉及 IR 與后端,同時(shí) go 官方還提供了大量開箱即用的庫 go/ast[2]

type?Node?interface?{
?Pos()?token.Pos?//?position?of?first?character?belonging?to?the?node
?End()?token.Pos?//?position?of?first?character?immediately?after?the?node
}
所有實(shí)現(xiàn) Pos End 的都是 Node
Comments注釋, //-style 或是 /*-styleDeclarations聲明,GenDecl(generic declaration node) 代表 import, constant, type 或 variable declaration.BadDecl代表有語法錯(cuò)誤的 nodeStatements常見的語句表達(dá)式,return, case, if 等等File代表一個(gè) go 源碼文件Package代表一組源代碼文件Expr表達(dá)式 ArrayExpr, StructExpr, SliceExpr 等等
我們來看一個(gè)例子吧,goast可視化界面[3] 更直觀一些
//?Manager?...
type?Manager?struct?{
?Same??????string
?All???????bool???`json:"all"`
?Version???int????`json:"-"`
?NormalStruct??pkgcmd.RootApp
?PointerStruct?*pkgcmd.RootApp
?SlicesField???????[]int
?MapField???????????map[string]string
}
我們定義結(jié)構(gòu)體 Manager 來看一下 goast 輸出結(jié)果
29??.??1:?*ast.GenDecl?{
30??.??.??Doc:?nil
31??.??.??TokPos:?foo:7:1
32??.??.??Tok:?type
33??.??.??Lparen:?-
34??.??.??Specs:?[]ast.Spec?(len?=?1)?{
35??.??.??.??0:?*ast.TypeSpec?{
36??.??.??.??.??Doc:?nil
37??.??.??.??.??Name:?*ast.Ident?{
38??.??.??.??.??.??NamePos:?foo:7:6
39??.??.??.??.??.??Name:?"Manager"
40??.??.??.??.??.??Obj:?*ast.Object?{
41??.??.??.??.??.??.??Kind:?type
42??.??.??.??.??.??.??Name:?"Manager"
43??.??.??.??.??.??.??Decl:?*(obj?@?35)
44??.??.??.??.??.??.??Data:?nil
45??.??.??.??.??.??.??Type:?nil
46??.??.??.??.??.??}
47??.??.??.??.??}
*ast.GenDecl 通用聲明,*ast.TypeSpec 代表是個(gè)類型的定義,名稱是 Manager
48????.??Assign:?-
49????.??Type:?*ast.StructType?{
50????.??.??Struct:?foo:7:14
51????.??.??Fields:?*ast.FieldList?{
52????.??.??.??Opening:?foo:7:21
53????.??.??.??List:?[]*ast.Field?(len?=?7)?{
54????.??.??.??.??0:?*ast.Field?{
55????.??.??.??.??.??Doc:?nil
56????.??.??.??.??.??Names:?[]*ast.Ident?(len?=?1)?{
57????.??.??.??.??.??.??0:?*ast.Ident?{
58????.??.??.??.??.??.??.??NamePos:?foo:8:2
59????.??.??.??.??.??.??.??Name:?"Same"
60????.??.??.??.??.??.??.??Obj:?*ast.Object?{
61????.??.??.??.??.??.??.??.??Kind:?var
62????.??.??.??.??.??.??.??.??Name:?"Same"
63????.??.??.??.??.??.??.??.??Decl:?*(obj?@?54)
64????.??.??.??.??.??.??.??.??Data:?nil
65????.??.??.??.??.??.??.??.??Type:?nil
66????.??.??.??.??.??.??.??}
67????.??.??.??.??.??.??}
68????.??.??.??.??.??}
69????.??.??.??.??.??Type:?*ast.Ident?{
70????.??.??.??.??.??.??NamePos:?foo:8:12
71????.??.??.??.??.??.??Name:?"string"
72????.??.??.??.??.??.??Obj:?nil
73????.??.??.??.??.??}
74????.??.??.??.??.??Tag:?nil
75????.??.??.??.??.??Comment:?nil
76????.??.??.??.??}
77????.??.??.??.??1:
*ast.StructType 代表類型是結(jié)構(gòu)體,*ast.Field 數(shù)組保存結(jié)構(gòu)體成員聲明,一共 7 個(gè)元素,第 0 個(gè)字段名稱 Same, 類型 string
131??.??3:?*ast.Field?{
132??.??.??Doc:?nil
133??.??.??Names:?[]*ast.Ident?(len?=?1)?{
134??.??.??.??0:?*ast.Ident?{
135??.??.??.??.??NamePos:?foo:11:2
136??.??.??.??.??Name:?"NormalStruct"
137??.??.??.??.??Obj:?*ast.Object?{
138??.??.??.??.??.??Kind:?var
139??.??.??.??.??.??Name:?"NormalStruct"
140??.??.??.??.??.??Decl:?*(obj?@?131)
141??.??.??.??.??.??Data:?nil
142??.??.??.??.??.??Type:?nil
143??.??.??.??.??}
144??.??.??.??}
145??.??.??}
146??.??.??Type:?*ast.SelectorExpr?{
147??.??.??.??X:?*ast.Ident?{
148??.??.??.??.??NamePos:?foo:11:16
149??.??.??.??.??Name:?"pkgcmd"
150??.??.??.??.??Obj:?nil
151??.??.??.??}
152??.??.??.??Sel:?*ast.Ident?{
153??.??.??.??.??NamePos:?foo:11:23
154??.??.??.??.??Name:?"RootApp"
155??.??.??.??.??Obj:?nil
156??.??.??.??}
157??.??.??}
158??.??.??Tag:?nil
159??.??.??Comment:?nil
160??.??}
*ast.SelectorExpr 代表該字段類型是 A.B,其中 A 代表 package, 具體 B 是什么類型不知道,還需要遍歷包 A
221??.??6:?*ast.Field?{
222??.??.??Doc:?nil
223??.??.??Names:?[]*ast.Ident?(len?=?1)?{
224??.??.??.??0:?*ast.Ident?{
225??.??.??.??.??NamePos:?foo:14:2
226??.??.??.??.??Name:?"MapField"
227??.??.??.??.??Obj:?*ast.Object?{
228??.??.??.??.??.??Kind:?var
229??.??.??.??.??.??Name:?"MapField"
230??.??.??.??.??.??Decl:?*(obj?@?221)
231??.??.??.??.??.??Data:?nil
232??.??.??.??.??.??Type:?nil
233??.??.??.??.??}
234??.??.??.??}
235??.??.??}
236??.??.??Type:?*ast.MapType?{
237??.??.??.??Map:?foo:14:21
238??.??.??.??Key:?*ast.Ident?{
239??.??.??.??.??NamePos:?foo:14:25
240??.??.??.??.??Name:?"string"
241??.??.??.??.??Obj:?nil
242??.??.??.??}
243??.??.??.??Value:?*ast.Ident?{
244??.??.??.??.??NamePos:?foo:14:32
245??.??.??.??.??Name:?"string"
246??.??.??.??.??Obj:?nil
247??.??.??.??}
248??.??.??}
249??.??.??Tag:?nil
250??.??.??Comment:?nil
251??.??}
252??}
*ast.MapType 代表類型是字段,Key, Value 分別定義鍵值類型
內(nèi)容有點(diǎn)多,大家感興趣自行實(shí)驗(yàn)
遍歷
看懂了 go ast 相關(guān)基礎(chǔ),我們就可以遍歷獲取結(jié)構(gòu)體樹形結(jié)構(gòu),廣度 + 深度相結(jié)合
func?(p?*Parser)?IterateGenNeighbours(dir?string)?{
?path,?err?:=?filepath.Abs(dir)
?if?err?!=?nil?{
??return
?}
?p.visitedPkg[dir]?=?true
?pkgs,?err?:=?parser.ParseDir(token.NewFileSet(),?path,?filter,?0)
?if?err?!=?nil?{
??return
?}
?todo?:=?map[string]struct{}{}
?for?pkgName,?pkg?:=?range?pkgs?{
??nbv?:=?NewNeighbourVisitor(path,?p,?todo,?pkgName)
??for?_,?astFile?:=?range?pkg.Files?{
???ast.Walk(nbv,?astFile)
??}
??//?update?import?specs?per?file
??for?name?:=?range?nbv.locals?{
???fmt.Sprintf("IterateGenNeighbours?find?struct:%s?pkg:%s?path:%s\n",?name,?nbv.locals[name].importPkg,?nbv.locals[name].importPath)
???nbv.locals[name].importSpecs?=?nbv.importSpec
??}
?}
?for?path?:=?range?todo?{
??dir?:=?os.Getenv("GOPATH")?+?"/src/"?+?strings.Replace(path,?"\"",?"",?-1)
??if?_,?visited?:=?p.visitedPkg[dir];?visited?{
???continue
??}
??p.IterateGenNeighbours(dir)
?}
}
這里的工作量比較大,涉及 import 包,調(diào)試了很久,有些 linter 只需讀單一文件即可,工作量沒法比
模板輸出
最后一步就是輸出結(jié)果,這里要 BFS 廣度遍歷結(jié)構(gòu)體樹,然后渲染模板
var?convertSlicePointerScalarTemplateString?=?`
????{%?if?ArrayLength?==?""?%}
????dst.{{?TargetFieldName?}}?=?make([]{{?TargetType?}},?len(src.{{?SrcFieldName?}}))
????{%?endif?%}
????for?i?:=?range?src.{{?SrcFieldName?}}?{
?????if?src.{{?SrcFieldName?}}[i]?==?nil?{
??????continue
?????}
?????tmp?:=?*src.{{?SrcFieldName?}}[i]?
?????dst.{{?TargetFieldName?}}[i]?=?&tmp
????}
上面是轉(zhuǎn)換 [8]*Scalar 可以是數(shù)組或切片,模板使用 pongo2[4] 實(shí)現(xiàn)的 jinji2 語法,非常強(qiáng)大
//?ConvertDtoInsuranceOptionToCommonInsuranceOptionV2?only?convert?exported?fields
func?ConvertDtoInsuranceOptionToCommonInsuranceOptionV2(src?*dto.InsuranceOption)?*common.InsuranceOptionV2?{
????if?src?==?nil?{
????????return?nil
????}
????dst?:=?&common.InsuranceOptionV2{}
????dst.ID?=?src.ID
????dst.OptionPremium?=?src.OptionPremium
????dst.InsuranceSignature?=?src.InsuranceSignature
????dst.Title?=?src.Title
????dst.Subtitle?=?src.Subtitle
????dst.ErrorText?=?src.ErrorText
????dst.IsIncluded?=?src.IsIncluded
????starCurrency?:=?ConvertDtoCurrencyDTOToCommonCurrencyDTO(src.Currency)
????if?starCurrency?!=?nil?{
????????dst.Currency?=?*starCurrency
????}
????return?dst
}
上面是輸出結(jié)果的樣例,整體來講比手寫靠譜多了,遇到個(gè)別 case 還是需要手工 fix
AST 其它應(yīng)用場景
1. 規(guī)則
工作當(dāng)中用到編譯原理的場景非常多,比如去年高老板分享的用規(guī)則引擎讓你一天上線十個(gè)需求
If?aa.bb.cc?==?1??//?說明是多車型發(fā)單
??Unmarshal(bb.cc.ee)
??看type是否為?4?
else??//?單車型發(fā)單
?Unmarshal(bb.cc.ff)
??看type是否為?4?
(type?=?4?的是拼車)
業(yè)務(wù)需要多種多樣,訂閱 MQ 根據(jù)需求做各種各樣的統(tǒng)計(jì),入庫,供業(yè)務(wù)查詢。如果業(yè)務(wù)類型少還好,但是 DIDI 業(yè)務(wù)復(fù)雜,如果每次都人工手寫 go 代碼效率太低
最后解決思路是 JPATH + Expression Eval, 需求只需要寫表達(dá)式,服務(wù)解析表達(dá)示即可。Eval 庫也是現(xiàn)成的 govaluate[5]
2. 模板
jinja2 就是這類的代表

原理非常簡單,感興趣的可以看官方實(shí)現(xiàn)
3. Inject 代碼
這里要介紹兩個(gè)項(xiàng)目 pingcap failpoint[6] 和 uber-go 的 gopatch

failpoint 實(shí)現(xiàn)很簡單,代碼里寫 Marker 函數(shù),這些空函數(shù)在正常編譯時(shí)會(huì)被編譯器優(yōu)化去掉,所以正常運(yùn)行時(shí) zero-cost
var?outerVar?=?"declare?in?outer?scope"
failpoint.Inject("failpoint-name-for-demo",?func(val?failpoint.Value)?{
????fmt.Println("unit-test",?val,?outerVar)
})
故障注入時(shí)通過 failctl 將 Marker 函數(shù)轉(zhuǎn)換為故障注入函數(shù),這里就用到了 go-ast 做劫持轉(zhuǎn)換
uber-go 的 gopatch 也非常強(qiáng)大,假如你的代碼有很多 go func 開啟的 goroutine, 你想批量加入 recover 邏輯,如果數(shù)據(jù)特別多人工加很麻煩,這時(shí)可以用 gopatcher
var?patchTemplateString?=?`@@
@@
+?import?"runtime/debug"
+?import?"{{?Logger?}}"
+?import?"{{?Statsd?}}"
go?func(...)?{
+????defer?func(){
+????????if?err?:=?recover();?err?!=?nil?{
+????????????statsd.Count1("{{?StatsdTag?}}",?"{{?FileName?}}")
+????????????logging.Error("{{?LoggerTag?}}",?"{{?FileName?}}?recover?from?panic,?err=%+v,?stack=%v",?err,?string(debug.Stack()))
+????????}
+????}()
???...
?}()
`
編寫模板,上面的例子自動(dòng)在 go func(...) { 開頭注入 recover 語句塊,非常方便
這個(gè)庫能做的事情特別多,感興趣自行實(shí)驗(yàn)
4. linter
大部分 linter 工具都是用 go ast 實(shí)現(xiàn)的,比如對(duì)于大寫的 Public 函數(shù),如果沒有注釋報(bào)錯(cuò)
//?BuildArgs?write?a
func?BuildArgs()?{
????var?a?int
????a?=?a?+?bbb.c
????return?a
}
我們看下該代碼的 ast 代碼
29??.??.??1:?*ast.FuncDecl?{
30??.??.??.??Doc:?*ast.CommentGroup?{
31??.??.??.??.??List:?[]*ast.Comment?(len?=?1)?{
32??.??.??.??.??.??0:?*ast.Comment?{
33??.??.??.??.??.??.??Slash:?foo:7:1
34??.??.??.??.??.??.??Text:?"http://?BuildArgs?write?a"
35??.??.??.??.??.??}
36??.??.??.??.??}
37??.??.??.??}
38??.??.??.??Recv:?nil
39??.??.??.??Name:?*ast.Ident?{
40??.??.??.??.??NamePos:?foo:8:6
41??.??.??.??.??Name:?"BuildArgs"
42??.??.??.??.??Obj:?*ast.Object?{
43??.??.??.??.??.??Kind:?func
44??.??.??.??.??.??Name:?"BuildArgs"
45??.??.??.??.??.??Decl:?*(obj?@?29)
46??.??.??.??.??.??Data:?nil
47??.??.??.??.??.??Type:?nil
48??.??.??.??.??}
49??.??.??.??}
linter 只需要檢查 FuncDecl 的 Name 如果是可導(dǎo)出的,同時(shí) Doc.CommentGroup 不存在,或是注釋不以函數(shù)名開頭,報(bào)錯(cuò)即可
另外如果大家對(duì)代碼 cycle 有要求,那么是不是可以 ast 掃一遍來發(fā)現(xiàn)呢?如果大家要求函數(shù)不能超過 100 行,是不是也可以實(shí)現(xiàn)呢?
玩法很多 ^^
小結(jié)
編譯原理雖然難,但是搞業(yè)務(wù)的只需要前端知識(shí)即可,不用研究的太深,有需要的場景,知道 AST 如何解決問題就行
今天的分享就這些,寫文章不容易,如果對(duì)大家有所幫助和啟發(fā),請(qǐng)大家?guī)兔c(diǎn)擊再看,點(diǎn)贊,分享 三連
關(guān)于 Go AST 大家有什么看法,歡迎留言一起討論,大牛多留言 ^_^
參考資料
go-cmp: https://github.com/google/go-cmp,
[2]go ast: https://pkg.go.dev/go/ast,
[3]goast-viewer 可視化界面: https://yuroyoro.github.io/goast-viewer/index.html,
[4]go pongo2 jinja2: github.com/flosch/pongo2,
[5]govaluate: https://github.com/Knetic/govaluate,
[6]pingcap failpoint: https://github.com/pingcap/failpoint,
