<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

          共 20127字,需瀏覽 41分鐘

           ·

          2022-07-11 12:45

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

          1. 結構體 dto 做 diff, 對比結果
          2. 自定義的結構體與 protobuf 生成的互相轉換,基于 json tag

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

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

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

          我們當前的方案是通過解析 ast, 讀源碼生成結構體樹,然后 BFS 遍歷自動生成轉換代碼

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

          結合 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
          }

          所有實現 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
          }

          我們定義結構體 Manager 來看一下 goast 輸出結果

          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 代表類型是結構體,*ast.Field 數組保存結構體成員聲明,一共 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 分別定義鍵值類型

          內容有點多,大家感興趣自行實驗

          遍歷

          看懂了 go ast 相關基礎,我們就可以遍歷獲取結構體樹形結構,廣度 + 深度相結合

          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 包,調試了很久,有些 linter 只需讀單一文件即可,工作量沒法比

          模板輸出

          最后一步就是輸出結果,這里要 BFS 廣度遍歷結構體樹,然后渲染模板

          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
              }

          上面是轉換 [8]*Scalar 可以是數組或切片,模板使用 pongo2[4] 實現的 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
          }

          上面是輸出結果的樣例,整體來講比手寫靠譜多了,遇到個別 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 根據需求做各種各樣的統(tǒng)計,入庫,供業(yè)務查詢。如果業(yè)務類型少還好,但是 DIDI 業(yè)務復雜,如果每次都人工手寫 go 代碼效率太低

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

          2. 模板

          jinja2 就是這類的代表

          原理非常簡單,感興趣的可以看官方實現

          3. Inject 代碼

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

          failpoint 實現很簡單,代碼里寫 Marker 函數,這些空函數在正常編譯時會被編譯器優(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 函數轉換為故障注入函數,這里就用到了 go-ast 做劫持轉換

          uber-go 的 gopatch 也非常強大,假如你的代碼有很多 go func 開啟的 goroutine, 你想批量加入 recover 邏輯,如果數據特別多人工加很麻煩,這時可以用 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 實現的,比如對于大寫的 Public 函數,如果沒有注釋報錯

          // 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 不存在,或是注釋不以函數名開頭,報錯即可

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

          玩法很多 ^^

          小結

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

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

          關于 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,


          瀏覽 80
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  伊人久久大香线蕉av一区 | 苍井空二区 | 91激情在线 | 女人一级毛片内射 | 国产婷婷在线视频 |