<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          每個 gopher 都需要了解的 Go AST

          共 9748字,需瀏覽 20分鐘

           ·

          2022-04-26 13:43

          最近業(yè)務遷移,大約 100+ 個接口需要從舊的服務,遷到公司框架。遇到幾個痛點:

          1. 結(jié)構(gòu)體 dto 做 diff, 對比結(jié)果
          2. 自定義的結(jié)構(gòu)體與 protobuf 生成的互相轉(zhuǎn)換,基于 json tag

          這類工作要么手寫(編譯期), 要么 reflect 反射實現(xiàn)(運行時)。其中 #1 考濾到性能問題,手寫最優(yōu),但是結(jié)構(gòu)體太大,同時 100+ 個接口遷移,工作量可以想象

          google 開源的 go-cmp[1], 輸出美觀,反射性能開銷大了點。當前業(yè)務大量使用,堆機器吧又不是不能用

          #2 目前不好解決,可以簡單的 json Marshal 再 Unmarshal, 但有些字段類型不一致,同時如何做 json tag 到 pb tag 轉(zhuǎn)換呢?

          我們當前的方案是通過解析 ast, 讀源碼生成結(jié)構(gòu)體樹,然后 BFS 遍歷自動生成轉(zhuǎn)換代碼

          //go:generate ast-tools --action convert --target-pkg aaa/dto/geresponse --src-pkg bbb/dto --source aaaResponse ?--target bbbResponse

          結(jié)合 go generate 自動生成,這是我們的目標

          Go AST 基礎

          不搞編譯器的大多只需要懂前端,不涉及 IR 與后端,同時 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
          }

          所有實現(xiàn) Pos End 的都是 Node

          • Comments 注釋, //-style 或是 /*-style
          • Declarations 聲明,GenDecl (generic declaration node) 代表 import, constant, type 或 variable declaration. BadDecl 代表有語法錯誤的 node
          • Statements 常見的語句表達式,return, case, if 等等
          • File 代表一個 go 源碼文件
          • Package 代表一組源代碼文件
          • Expr 表達式 ArrayExpr, StructExpr, SliceExpr 等等

          我們來看一個例子吧,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 代表是個類型的定義,名稱是 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 個元素,第 0 個字段名稱 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)容有點多,大家感興趣自行實驗

          遍歷

          看懂了 go ast 相關(guān)基礎,我們就可以遍歷獲取結(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] 實現(xiàn)的 jinji2 語法,非常強大

          //?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é)果的樣例,整體來講比手寫靠譜多了,遇到個別 case 還是需要手工 fix

          AST 其它應用場景

          1. 規(guī)則

          工作當中用到編譯原理的場景非常多,比如去年高老板分享的用規(guī)則引擎讓你一天上線十個需求

          If?aa.bb.cc?==?1??//?說明是多車型發(fā)單
          ??Unmarshal(bb.cc.ee)
          ??看type是否為?4?
          else??//?單車型發(fā)單
          ?Unmarshal(bb.cc.ff)
          ??看type是否為?4?
          (type?=?4?的是拼車)

          業(yè)務需要多種多樣,訂閱 MQ 根據(jù)需求做各種各樣的統(tǒng)計,入庫,供業(yè)務查詢。如果業(yè)務類型少還好,但是 DIDI 業(yè)務復雜,如果每次都人工手寫 go 代碼效率太低

          最后解決思路是 JPATH + Expression Eval, 需求只需要寫表達式,服務解析表達示即可。Eval 庫也是現(xiàn)成的 govaluate[5]

          2. 模板

          jinja2 就是這類的代表

          原理非常簡單,感興趣的可以看官方實現(xiàn)

          3. Inject 代碼

          這里要介紹兩個項目 pingcap failpoint[6] 和 uber-go 的 gopatch

          failpoint 實現(xiàn)很簡單,代碼里寫 Marker 函數(shù),這些空函數(shù)在正常編譯時會被編譯器優(yōu)化去掉,所以正常運行時 zero-cost

          var?outerVar?=?"declare?in?outer?scope"
          failpoint.Inject("failpoint-name-for-demo",?func(val?failpoint.Value)?{
          ????fmt.Println("unit-test",?val,?outerVar)
          })

          故障注入時通過 failctl 將 Marker 函數(shù)轉(zhuǎn)換為故障注入函數(shù),這里就用到了 go-ast 做劫持轉(zhuǎn)換

          uber-go 的 gopatch 也非常強大,假如你的代碼有很多 go func 開啟的 goroutine, 你想批量加入 recover 邏輯,如果數(shù)據(jù)特別多人工加很麻煩,這時可以用 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()))
          +????????}
          +????}()
          ???...
          ?}()
          `

          編寫模板,上面的例子自動在 go func(...) { 開頭注入 recover 語句塊,非常方便

          這個庫能做的事情特別多,感興趣自行實驗

          4. linter

          大部分 linter 工具都是用 go ast 實現(xiàn)的,比如對于大寫的 Public 函數(shù),如果沒有注釋報錯

          //?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 如果是可導出的,同時 Doc.CommentGroup 不存在,或是注釋不以函數(shù)名開頭,報錯即可

          另外如果大家對代碼 cycle 有要求,那么是不是可以 ast 掃一遍來發(fā)現(xiàn)呢?如果大家要求函數(shù)不能超過 100 行,是不是也可以實現(xiàn)呢?

          玩法很多 ^^

          小結(jié)

          編譯原理雖然難,但是搞業(yè)務的只需要前端知識即可,不用研究的太深,有需要的場景,知道 AST 如何解決問題就行

          今天的分享就這些,寫文章不容易,如果對大家有所幫助和啟發(fā),請大家?guī)兔c擊再看,點贊,分享 三連

          關(guān)于 Go AST 大家有什么看法,歡迎留言一起討論,大牛多留言 ^_^

          參考資料

          [1]

          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,



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復?ebook?獲取;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。

          瀏覽 64
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  影音先锋91麻豆 | 狼人伊人网站 | 蜜芽人妻熟女av一区二区三区 | 精品无码久久久久久久久不卡 | 男女免费视频网站 |