17

编译原理有啥用之Go语言懒人工具

 5 years ago
source link: https://studygolang.com/articles/16142?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

动机

笔者在使用Go语言进行开发的过程中发现一些机械化重构代码的需求,而IDE(Goland)没有相应的功能,导致每次都需要手动写,非常不便。举两个例子:

例子1

type EsNginxLogInfo struct {
    RemoteAddr           string     
    RemoteUser           string     
    TimeLocal            string     
    Host                 string     
    Method               string     
    Path                 string     
    Query                url.Values 
    Status               string     
    BytesSent            string     
    HttpReferer          string     
    HttpUserAgent        string     
    HttpXForwardedFor    string     
    UpstreamResponseTime string     
    RequestTime          string     
    Time                 time.Time
}

为了转json给前端以及从前端接收参数,需要增加json和url以及require的tag,然后10分钟过去了,得到:

type EsNginxLogInfo struct {
    RemoteAddr           string     `json:"remote_addr" url:"remote_addr" require:"true"`                       
    RemoteUser           string     `json:"remote_user" url:"remote_user" require:"true"`                       
    TimeLocal            string     `json:"time_local" url:"time_local" require:"true"`                         
    Host                 string     `json:"host" url:"host" require:"true"`                                     
    Method               string     `json:"method" url:"method" require:"true"`                                 
    Path                 string     `json:"path" url:"path" require:"true"`                                     
    Query                url.Values `json:"query" url:"query" require:"true"`                                   
    Status               string     `json:"status" url:"status" require:"true"`                                 
    BytesSent            string     `json:"bytes_sent" url:"bytes_sent" require:"true"`                         
    HttpReferer          string     `json:"http_referer" url:"http_referer" require:"true"`                     
    HttpUserAgent        string     `json:"http_user_agent" url:"http_user_agent" require:"true"`               
    HttpXForwardedFor    string     `json:"http_x_forwarded_for" url:"http_x_forwarded_for" require:"true"`     
    UpstreamResponseTime string     `json:"upstream_response_time" url:"upstream_response_time" require:"true"` 
    RequestTime          string     `json:"request_time" url:"request_time" require:"true"`                     
    Time                 time.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
}

可见要额外写很多代码。类似的需求还有:

  1. MarshalJSON和UnMarshalJSON代码;
  2. ORM的update代码;
  3. 转结构变量代码;
  4. 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

最后顺便实现了下词法着色和代码对齐,得到这个效果:

QZriIjA.png!web

另外改进了下生成的update函数,避免单行过长的情况(为增加可读性,不能在单词中间断开。。仿佛回到了当年刷leetCode的时光):

QRveE3F.png!web

工具地址

http://xndh.net/go


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK