編譯原理有啥用之Go語言懶人工具
動機
筆者在使用Go語言進行開發的過程中發現一些機械化重構程式碼的需求,而IDE(Goland)沒有相應的功能,導致每次都需要手動寫,非常不便。舉兩個例子:
例子1
type EsNginxLogInfo struct { RemoteAddrstring RemoteUserstring TimeLocalstring Hoststring Methodstring Pathstring Queryurl.Values Statusstring BytesSentstring HttpRefererstring HttpUserAgentstring HttpXForwardedForstring UpstreamResponseTime string RequestTimestring Timetime.Time }
為了轉json給前端以及從前端接收引數,需要增加json和url以及require的tag,然後10分鐘過去了,得到:
type EsNginxLogInfo struct { RemoteAddrstring`json:"remote_addr" url:"remote_addr" require:"true"` RemoteUserstring`json:"remote_user" url:"remote_user" require:"true"` TimeLocalstring`json:"time_local" url:"time_local" require:"true"` Hoststring`json:"host" url:"host" require:"true"` Methodstring`json:"method" url:"method" require:"true"` Pathstring`json:"path" url:"path" require:"true"` Queryurl.Values `json:"query" url:"query" require:"true"` Statusstring`json:"status" url:"status" require:"true"` BytesSentstring`json:"bytes_sent" url:"bytes_sent" require:"true"` HttpRefererstring`json:"http_referer" url:"http_referer" require:"true"` HttpUserAgentstring`json:"http_user_agent" url:"http_user_agent" require:"true"` HttpXForwardedForstring`json:"http_x_forwarded_for" url:"http_x_forwarded_for" require:"true"` UpstreamResponseTime string`json:"upstream_response_time" url:"upstream_response_time" require:"true"` RequestTimestring`json:"request_time" url:"request_time" require:"true"` Timetime.Time`json:"time" url:"time" require:"true"` }
例子2
隨著業務的擴充套件,函式在改動時引數可能越來越多,為避免順序問題,希望重構過多引數的函式為單引數。例如重構前:
func fun1() { structA := 0 structB := 0 structC := 0 structD := 0 structE := 0 structF := 0 structG := 0 fun2(structA,structB,structC,structD,structE,structF,structG) } func fun2(structA *StructA, structB *StructB, structC *StructC, structD *StructD, structE *StructE,structF *StructF, structG *StructG) { }
重構後:
// 新建的input結構 type CombinedInputs struct { StructA *StructA StructB *StructB StructC *StructC StructD *StructD StructE *StructE StructF *StructF StructG *StructG } func fun1() { structA := 0 structB := 0 structC := 0 structD := 0 structE := 0 structF := 0 structG := 0 // 打包 combinedInputs := &CombinedInputs{ StructA: structA, StructB: structB, StructC: structC, StructD: structD, StructE: structE, StructF: structF, StructG: structG, } fun2(combinedInputs) } func fun2(combinedInputs *CombinedInputs) { // 解包 structA := combinedInputs.StructA structB := combinedInputs.StructB structC := combinedInputs.StructC structD := combinedInputs.StructD structE := combinedInputs.StructE structF := combinedInputs.StructF structG := combinedInputs.StructG }
可見要額外寫很多程式碼。類似的需求還有:
- MarshalJSON和UnMarshalJSON程式碼;
- ORM的update程式碼;
- 轉結構變數程式碼;
- struct轉json(寫api文件用)。
解決方案
解決方案就是做一個即開即用的網頁,輸入待重構的程式碼,點選按鈕實現程式碼的轉換。關鍵在於核心演算法如何實現?
第一次嘗試
一開始嘗試用正則表示式去匹配,來獲得struct欄位定義的各個組成部分,於是寫出了這個噁心的正則:
/^\s*?([^\s]+)\s+([^\s]+)\s*?([^\s]*?)(`.+?`\s*)?(\/\/.*?)?$/
結果發現,這還只能匹配簡單的情況,如果遇到輸入為:
FoodId ***[/* test*/]map[a./*test*/C]/*test*/[/*test*/]*[]package.Int `pk:"true"` // test
它就gg了。
第二次嘗試
最後用上了編譯原理的知識:根據Golang struct的詞法、語法定義,用遞迴下降編寫詞法分析器、語法分析器,產生抽象語法樹,然後就可以精確地得到任意一項,從而happy地實現程式碼重構了。其中,摸索著寫出的語法定義的EBNF為:
<NodeCode> ::= {TokenNewLine} {<NodeStructDefine>{TokenNewLine}}[<NodeMultiParamtersDecl>{TokenNewLine}] TokenEof <NodeStructDefine> ::= TokenKwType <NodeStructName> TokenKwStruct TokenLeftBrace {<NodeStructFieldDefine>} TokenRightBrace (TokenNewLine|TokenEOF) <NodeStructName> ::= TokenIdentifier <NodeStructFieldDefine> ::= <NodeStructFieldName> <NodeStructFieldType> [<NodeStructFieldTag>] TokenNewLine <NodeStructFieldName> ::= TokenIdentifier <NodeStructFieldType> ::= <TypeIdentifier> <TypeIdentifier> ::= <Slice>|<Pointer>|<SimpleIdentifier>|<Map> <Slice> ::= "[""]"<TypeIdentifier> <Pointer> ::= TokenStar<TypeIdentifier> <SimpleIdentifier> ::= [<PackageName>.]TokenIdentifier <PackageName> ::= TokenIdentifier <Map> ::= TokenKwMap"["<MapKey>"]"<TypeIdentifier> <MapKey> ::= <Pointer>|<SimpleIdentifier> //map key不能是function ,map, slice,可以是指標 <NodeStructFieldTag> ::= TokenReverseQuote {<NodeStructFieldTagKey>:<NodeStructFieldTagValue>} TokenReverseQuote <NodeStructFieldTagKey> ::= TokenIdentifier <NodeStructFieldTagValue> ::= TokenDoubleQuoteString <NodeMultiParamtersDecl> ::= <NodeStructFieldName><NodeStructFieldType>{,<NodeStructFieldName><NodeStructFieldType>}
其中, NodeMultiParamtersDecl
是為了實現多引數重構功能而額外定義的文法符號,形式為 structA *StructA, structB *StructB, ..., structG *StructG
最後順便實現了下詞法著色和程式碼對齊,得到這個效果:

另外改進了下生成的update函式,避免單行過長的情況(為增加可讀性,不能在單詞中間斷開。。彷彿回到了當年刷leetCode的時光):

工具地址
ofollow,noindex">http://xndh.net/go