整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Golang-實現一個簡單的DSL解釋器

          Golang:實現一個簡單的DSL解釋器

          o-實現一個簡單的DSL

          什么是DSL

          DSL 是 Domain Specific Language 的縮寫,中文翻譯為領域特定語言(下簡稱 DSL);而與 DSL 相對的就是 GPL,這里的 GPL 并不是我們知道的開源許可證,而是 General Purpose Language 的簡稱,即通用編程語言,也就是我們非常熟悉的 Objective-C、Java、Python 以及 C 語言等等。

          簡單說,就是為了解決某一類任務而專門設計的計算機語言。

          • Regex
          • SQL
          • HTML&CSS

          共同特點

          沒有計算和執行的概念;

          • 其本身并不需要直接表示計算;
          • 使用時只需要聲明規則、事實以及某些元素之間的層級和關系;
          • 總結起來一句話:表達能力有限,通過在表達能力上做的妥協換取在某一領域內的高效 那么DSL解釋器的主要功能是解釋執行DSL

          設計原則

          實現DSL總共需要完成兩部分工作:

          設計語法和語義,定義 DSL 中的元素是什么樣的,元素代表什么意思 實現 parser,對 DSL 解析,最終通過解釋器來執行 那么我們可以得到DSL的設計原則:

          簡單

          • 學習成本低,DSL語法最好和部門主要技術棧語言保持一致(go,php)
          • 語法簡單,刪減了golang大部分的語法,只支持最基本的數據格式,二元運算符,控制語句少量的語法糖嵌入式DSL
          • DSL需要嵌入到現有的編程語言中,發揮其實時解釋執行且部署靈活的特點
          • 使用json類型的context與外部系統進行通信,且提供與context操作相關的語法糖

          解釋器工作流程

          大部分編譯器的工作可以被分解為三個主要階段:解析(Parsing),轉化(Transformation)以及 代碼生成(Code Generation)

          • 解析 將源代碼轉換為一個更抽象的形式。
          • 轉換 接受解析產生的抽象形式并且操縱這些抽象形式做任何編譯器想讓它們做的事。
          • 代碼生成 基于轉換后的代碼表現形式(code representation)生成目標代碼。

          解析

          • 詞法分析 —— tokenizer 通過一個叫做tokenizer(詞素生成器,也叫lexer)的工具將源代碼分解成一個個詞素。(詞素是描述編程語言語法的對象。它可以描述數字,標識符,標點符號,運算符等等。)
          • 語法分析 —— parser 接收詞素并將它們組合成一個描述了源代碼各部分之間關系的中間表達形式:抽象語法樹。(抽象語法樹是一個深度嵌套的對象,這個對象以一種既能夠簡單地操作又提供很多關于源代碼信息的形式,來展現代碼。)

          轉換

          • 這個過程接收解析生成的抽象語法樹并對它做出改動
          • 轉換階段可以改變抽象語法樹使代碼保持在同一個語言,或者編譯成另外一門語言。

          代碼生成

          • 生成新的代碼,一般是二進制或者匯編

          aki-DSL解釋器設計原理

          解析源代碼生成AST

          那么想要實現一個腳本解釋器的話,就需要實現上面的三個步驟,而且我們發現,承上啟下的是AST(抽象語法樹),它在解釋器中十分重要

          好在萬能的golang將parse api暴露給用戶了,可以讓我們省去一大部分工作去做語法解析得到AST,示例代碼如下:

          package main
          
          import (
          	"fmt"
          	"go/ast"
          	"go/parser"
          	"go/token"
          )
          
          func main() {
          	expr :=`a==1 && b==2`
          	fset :=token.NewFileSet()
          	exprAst, err :=parser.ParseExpr(expr)
          	if err !=nil {
          		fmt.Println(err)
          		return
          	}
          
          	ast.Print(fset, exprAst)
          }

          得到的結果:

          0  *ast.BinaryExpr {
               1  .  X: *ast.BinaryExpr {
               2  .  .  X: *ast.Ident {
               3  .  .  .  NamePos: -
               4  .  .  .  Name: "a"
               5  .  .  .  Obj: *ast.Object {
               6  .  .  .  .  Kind: bad
               7  .  .  .  .  Name: ""
               8  .  .  .  }
               9  .  .  }
              10  .  .  OpPos: -
              11  .  .  Op:==12  .  .  Y: *ast.BasicLit {
              13  .  .  .  ValuePos: -
              14  .  .  .  Kind: INT
              15  .  .  .  Value: "1"
              16  .  .  }
              17  .  }
              18  .  OpPos: -
              19  .  Op: &&
              20  .  Y: *ast.BinaryExpr {
              21  .  .  X: *ast.Ident {
              22  .  .  .  NamePos: -
              23  .  .  .  Name: "b"
              24  .  .  .  Obj: *(obj @ 5)
              25  .  .  }
              26  .  .  OpPos: -
              27  .  .  Op:==28  .  .  Y: *ast.BasicLit {
              29  .  .  .  ValuePos: -
              30  .  .  .  Kind: INT
              31  .  .  .  Value: "2"
              32  .  .  }
              33  .  }
              34  }

          并且,作為一個嵌入式的DSL,我們的設計是依托在golang代碼之上運行的,我們不需要代碼生成這一個步驟,直接使用golang來解析AST來執行相應的操作

          那么,我們的現在的工作就是如何解析AST并做相應的操作即可.

          解析AST

          AST的結構分析

          那么AST是什么結構呢,他大致可以分為如下結構

          1.ast.Decl

          All declaration nodes implement the Decl interface.

          var a int //GenDecl
          func main()  //FuncDecl

          2.ast.Stmt

          All statement nodes implement the Stmt interface.

          a :=1 //AssignStmt
          b :=map[string]string{"name":"nber1994", "age":"eghiteen"}
          
          if a > 2 { //IfStmt
          	b["age"]="18" //BlockStmt 
          } else {
          }
          
          for i:=0;i<10;i++ { //ForStmt
          }
          
          
          for k, v :=range b { //RangeStmt
          }
          return a //ReturnStmt

          3.ast.Expr

          All expression nodes implement the Expr interface.

          a :=1 //BasicLit
          b :="string"
          a=a + 1 //BinaryExpr
          b :=map[string]string{} //CompositLitExpr
          c :=Get("test.test") //CallExpr
          d :=b["name"] //IndexExpr

          主要思路

          通過分析AST結構我們知道,一個ast.Decl是由多個ast.Stmt,并且一個ast.Stmt是由多個ast.Expr組成的,簡單來說就是一個樹形結構,那么這么一來就好辦了,代碼大框架一定是遞歸。

          我們自底向上,分別實現對各種類型的ast.Expr,ast.Stmt, ast.Decl的解釋執行方法,并把解釋結果向上傳遞。然后通過一個根節點切入,遞歸方式從上向下解釋執行即可

          主要代碼:

          //編譯Expr
          func (this *Expr) CompileExpr(dct *dslCxt.DslCxt, rct *Stmt, r ast.Expr) interface{} {
              var ret interface{}
              if nil==r {
                  return ret
              }
              switch r :=r.(type) {
              case *ast.BasicLit: //基本類型
                  ret=this.CompileBasicLitExpr(dct, rct, r)
              case *ast.BinaryExpr: //二元表達式
                  ret=this.CompileBinaryExpr(dct, rct, r)
              case *ast.CompositeLit: //集合類型
                  switch  r.Type.(type) {
                  case *ast.ArrayType: //數組
                      ret=this.CompileArrayExpr(dct, rct, r)
                  case *ast.MapType: //map
                      ret=this.CompileMapExpr(dct, rct, r)
                  default:
                      panic("syntax error: nonsupport expr type")
                  }
              case *ast.CallExpr:
                  ret=this.CompileCallExpr(dct, rct, r)
              case *ast.Ident:
                  ret=this.CompileIdentExpr(dct, rct, r)
              case *ast.IndexExpr:
                  ret=this.CompileIndexExpr(dct, rct, r)
              default:
                  panic("syntax error: nonsupport expr type")
              }
              return ret
          }
          
          //編譯stmt
          func (this *Stmt) CompileStmt(cpt *CompileCxt, stmt ast.Stmt) {
              if nil==stmt {
                  return
              }
              cStmt :=this.NewChild()
              switch stmt :=stmt.(type) {
              case *ast.AssignStmt:
                  //賦值在本節點的內存中
                  this.CompileAssignStmt(cpt, stmt)
              case *ast.IncDecStmt:
                  this.CompileIncDecStmt(cpt, stmt)
              case *ast.IfStmt:
                  cStmt.CompileIfStmt(cpt, stmt)
              case *ast.ForStmt:
                  cStmt.CompileForStmt(cpt, stmt)
              case *ast.RangeStmt:
                  cStmt.CompileRangeStmt(cpt, stmt)
              case *ast.ReturnStmt:
                  cStmt.CompileReturnStmt(cpt, stmt)
              case *ast.BlockStmt:
                  cStmt.CompileBlockStmt(cpt, stmt)
              case *ast.ExprStmt:
                  cStmt.CompileExprStmt(cpt, stmt)
              default:
                  panic("syntax error: nonsupport stmt ")
              }
          }

          實現runtime context

          代碼的整體結構有了,那么對于DSL中聲明的變量存儲,以及局部變量的作用域怎么解決呢

          首先,從虛擬內存的結構我們得到啟發,可以使用hash表的結構來模擬最基本的內存空間以及存取操作,得益于golang的interface{},我們可以把不同數據類型的數據存入一個map[string]interface{}中得到一個范類型的數組,這樣我們就構建出了一個簡單的runtime memory的雛形。

          type RunCxt struct {
              Vars map[string]interface{}
              Name string
          }
          
          func NewRunCxt() *RunCxt{
              return &RunCxt{
                  Vars: make(map[string]interface{}),
              }
          }
          
          //獲取值
          func (this *RunCxt) GetValue(varName string) interface{}{
              if _, exist :=this.Vars[varName]; !exist {
                  panic("syntax error: not exist var")
              }
              return this.Vars[varName]
          }
          
          func (this *RunCxt) ValueExist(varName string) bool {
              _, exist :=this.Vars[varName]
              return exist
          }
          
          //設置值
          func (this *RunCxt) SetValue(varName string, value interface{}) bool {
              this.Vars[varName]=value
              return true
          }
          
          func (this *RunCxt) ToString() string {
              jsonStu, _ :=json.Marshal(this.Vars)
              return string(jsonStu)
          }

          那么,如何實現局部變量的作用域呢?

          package main
          
          func main() {
              a :=2
          
              for i:=0;i<10;i++ {
                  a++
                  b :=2
              }
              a=3
              b=3 //error b的聲明是在for語句中,外部是無法訪問的
          }

          那么,這個runtime context的位置就很重要,我們做如下處理:

          每個Stmt節點都有一個runtime context 寫入數據時,AssignStmt類型在本Stmt節點中賦值,其他類型新建一個Stmt子節點執行 讀取數據時,從本節點開始向上遍歷父節點,在runtime context中尋找變量,找到即止 通過這一機制,我們可以得到的效果是:

          同一個BlockStmt下的多個Stmt(IfStmt,ForStmt等)處理節點之間的runtime context是互相隔離的 每個子節點,都能訪問到父節點中定義的變量

          代碼實現:

          type Stmt struct{
              Rct *runCxt.RunCxt //變量作用空間
              Type int
              Father *Stmt //子節點可以訪問到父節點的內存空間
          }
          
          func NewStmt() *Stmt {
              rct :=runCxt.NewRunCxt()
              return &Stmt{
                  Rct: rct,
              }
          }
          
          func (this *Stmt) NewChild() *Stmt {
              stmt :=NewStmt()
              stmt.Father=this
              return stmt
          }
          
          //編譯stmt
          func (this *Stmt) CompileStmt(cpt *CompileCxt, stmt ast.Stmt) {
              if nil==stmt {
                  return
              }
              cStmt :=this.NewChild()
              switch stmt :=stmt.(type) {
              case *ast.AssignStmt:
                  //賦值在本節點的內存中
                  this.CompileAssignStmt(cpt, stmt)
              case *ast.IncDecStmt:
                  this.CompileIncDecStmt(cpt, stmt)
              case *ast.IfStmt:
                  cStmt.CompileIfStmt(cpt, stmt)
              case *ast.ForStmt:
                  cStmt.CompileForStmt(cpt, stmt)
              case *ast.RangeStmt:
                  cStmt.CompileRangeStmt(cpt, stmt)
              case *ast.ReturnStmt:
                  cStmt.CompileReturnStmt(cpt, stmt)
              case *ast.BlockStmt:
                  cStmt.CompileBlockStmt(cpt, stmt)
              case *ast.ExprStmt:
                  cStmt.CompileExprStmt(cpt, stmt)
              default:
                  panic("syntax error: nonsupport stmt ")
              }
          }

          變量類型與內部變量類型

          首先,嵌入式的是golang系統,為了和外部系統保持一個很好地數據類型交互以及數據的準確性,DSL最好也是強類型語言。但是為了簡單,我們會刪減一些數據類型,保留最基本且最穩定的數據類型

          func (this *Expr) CompileBasicLitExpr(cpt *CompileCxt, rct *Stmt, r *ast.BasicLit) interface{} {
              var ret interface{}
              switch r.Kind {
              case token.INT:
                  ret=cast.ToInt64(r.Value)
              case token.FLOAT:
                  ret=cast.ToFloat64(r.Value)
              case token.STRING:
                  retStr :=cast.ToString(r.Value)
                  var err error
                  ret, err=strconv.Unquote(retStr)
                  if nil !=err {
                      panic(fmt.Sprintf("syntax error: Bad String %v", cpt.Fset.Position(r.Pos())))
                  }
              default:
                  panic(fmt.Sprintf("syntax error: Bad BasicList Type %v", cpt.Fset.Position(r.Pos())))
              }
              return ret
          }
          
          func (this *Expr) CompileMapExpr(cpt *CompileCxt, rct *Stmt, r *ast.CompositeLit) interface{} {
              ret :=make(map[interface{}]interface{})
              var key interface{}
              var value interface{}
              for _, e :=range r.Elts {
                  key=this.CompileExpr(cpt, rct, e.(*ast.KeyValueExpr).Key)
                  value=this.CompileExpr(cpt, rct, e.(*ast.KeyValueExpr).Value)
                  ret[key]=value
              }
              return ret
          }
          
          func (this *Expr) CompileArrayExpr(cpt *CompileCxt, rct *Stmt, r *ast.CompositeLit) interface{} {
              var ret []interface{}
              for _, e :=range r.Elts {
                  switch e :=e.(type) {
                  case *ast.BasicLit:
                      ret=append(ret, this.CompileExpr(cpt, rct, e))
                  case *ast.CompositeLit:
                      //拼接結構體
                      compLit :=*.CompositeLit{
                          Type: r.Type.(*ast.ArrayType).Elt,
                          Elts: e.Elts,
                      }
                      ret=append(ret, this.CompileExpr(cpt, rct, compLit))
                  default:
                      panic(fmt.Sprintf("syntax error: Bad Array Item Type %v", cpt.Fset.Position(r.Pos())))
                  }
              }
              return ret
          }

          我們可以看到,DSL數據與go數據類型對應關系為:

          DSL數據類型go數據類型備注intint64最大范圍floatfloat64最大范圍stringstringmapmap[interface{}]interface{}最大容忍度array slice[]interface{}{}最大容忍度

          DSL與外部系統交互

          通過JsonMap與外部系統進行交互,且提供Get(path) Set(path)方法,去動態的訪問與修改Json context中的節點

          但是外部交互Json又是多種結構類型的,借助于nodejson可以解析動態json結構,通過XX.X格式的路徑,來動態的訪問和修改json中的字段

          解析CallExpr,通過reflect來調用內部函數

          func (this *Expr) CompileCallExpr(dct *dslCxt.DslCxt, rct *Stmt, r *ast.CallExpr) interface{} {
              var ret interface{}
              //校驗內置函數
              var funcArgs []reflect.Value
              funcName :=r.Fun.(*ast.Ident).Name
              //初始化入參
              for _, arg :=range r.Args {
                  funcArgs=append(funcArgs, reflect.ValueOf(this.CompileExpr(dct, rct, arg)))
              }
              var res []reflect.Value
              if RealFuncName, exist:=SupFuncList[funcName]; exist {
                  flib :=NewFuncLib()
                  res=reflect.ValueOf(flib).MethodByName(RealFuncName).Call(funcArgs)
              } else {
                  res=reflect.ValueOf(dct).MethodByName(funcName).Call(funcArgs)
              }
              if nil==res {
                  return ret
              }
              return res[0].Interface()
          }

          成果

          https://github.com/nber1994/akiDsl

          Testcontainers for Go使開發人員能夠輕松地針對容器化依賴項運行測試。在我們之前的文章中,您可以找到使用 Testcontainers 進行集成測試的介紹,并探索如何使用 Testcontainers(用 Java)編寫功能測試。

          這篇博文將深入探討如何使用模塊以及 Golang 測試容器的常見問題。

          我們用它做什么?

          服務經常使用外部依賴項,如數據存儲或隊列。可以模擬這些依賴項,但如果您想要運行集成測試,最好根據實際依賴項(或足夠接近)進行驗證。

          使用依賴項的映像啟動容器是驗證應用程序是否按預期運行的便捷方法。使用 Testcontainers,啟動容器是通過編程方式完成的,因此您可以將其定義為測試的一部分。運行測試的機器(開發人員、CI/CD)需要具有容器運行時接口(例如 Docker、Podman...)

          基本實現

          Testcontainers for Go 非常易于使用,快速啟動示例如下:

          ctx :=context.TODO()
          req :=testcontainers.ContainerRequest{
              Image:        "redis:latest",
              ExposedPorts: []string{"6379/tcp"},
              WaitingFor:   wait.ForLog("Ready to accept connections"),
          }
          redisC, err :=testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
              ContainerRequest: req,
              Started:          true,
          })
          if err !=nil {
              panic(err)
          }
          defer func() {
              if err :=redisC.Terminate(ctx); err !=nil {
                  panic(err)
              }
          }()
          

          如果我們深入研究上面的代碼,我們會注意到:

          1. testcontainers.ContainerRequest使用容器鏡像、暴露端口和等待策略參數初始化結構體
          2. testcontainers.GenericContainer啟動容器并返回容器和錯誤結構
          3. redisC.Terminatedefer測試完成后終止容器

          實現我們自己的內部庫

          從上一節的例子來看,存在一些小小的不便:

          1. wait.ForLog("Ready to accept connections")使用日志等待容器啟動,這很容易中斷
          2. ExposedPorts: []string{"6379/tcp"}需要了解 Redis 的暴露端口

          運行 Redis 容器可能還需要一些額外的環境變量和其他參數,這需要更深入的知識。因此,我們決定創建一個內部庫,該庫將使用簡化測試實施所需的默認參數初始化容器。為了保持靈活性,我們使用了功能選項模式,以便消費者仍然可以根據需要進行自定義。

          Redis 的實現示例:

          func defaultPreset() []container.Option {
              return []container.Option{
                  container.WithPort("6379/tcp"),
                  container.WithGetURL(func(port nat.Port) string {
                      return "localhost:" + port.Port()
                  }),
                  container.WithImage("redis"),
                  container.WithWaitingStrategy(func(c *container.Container) wait.Strategy {
                      return wait.ForAll(
                          wait.NewHostPortStrategy(c.Port),
                          wait.ForLog("Ready to accept connections"))
                  }),
              }
          }
          
          // New - create a new container able to run redis
          func New(options ...container.Option) (*container.Container, error) {
              c :=container.Container{}
              options=append(defaultPreset(), options...)
              for _, o :=range options {
                  o(&c)
              }
          
              return &c, nil
          }
          
          // Start - start a Redis container and return a container.CreatedContainer
          func Start(ctx context.Context, options ...container.Option) (container.CreatedContainer, error) {
              p, err :=New(options...)
              if err !=nil {
                  return container.CreatedContainer{}, err
              }
              return p.Start(ctx)
          }
          

          Redis 庫的使用:

          ctx :=context.TODO()
          cc, err :=redis.Start(ctx, container.WithVersion("latest"))
          if err !=nil {
              panic(err)
          }
          defer func() {
              if err :=cc.Stop(ctx, nil); err !=nil {
                  panic(err)
              }
          }()
          

          有了這個內部庫,開發人員可以輕松地為 Redis 添加測試,而無需弄清楚等待策略、暴露端口等。如果出現不兼容的情況,可以更新內部庫以集中修復問題。

          常見問題 - 垃圾收集器(Ryuk / Reaper)

          Testcontainers 還額外確保了測試完成后容器會被移除,它使用垃圾收集器defer,這是一個作為“sidecar”啟動的附加容器。即使測試崩潰(這將阻止運行),此容器也會負責停止正在測試的容器。

          當使用Docker時,它可以正常工作,但使用其他容器運行時接口(如Podman)時經常會遇到這種錯誤:Error response from daemon: container create: statfs /var/run/docker.sock: permission denied: creating reaper failed: failed to create container

          “修復此問題”的一種方法是使用環境變量將其停用TESTCONTAINERS_RYUK_DISABLED=true

          另一種方法是設置 Podman 機器 rootful 并添加:

          export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true; # needed to run Reaper (alternative disable it TESTCONTAINERS_RYUK_DISABLED=true)
          export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock; # needed to apply the bind with statfs
          

          在我們的內部庫中,我們采取默認禁用它的方法,因為開發人員在本地運行它時遇到了問題。

          轉向模塊

          一旦我們的內部庫足夠穩定,我們就決定是時候通過為 Testcontainers 做貢獻來回饋社區了。但令人驚訝的是…… Testcontainers 剛剛引入了模塊。模塊的功能與我們的內部庫完全一樣,因此我們將所有服務遷移到模塊并停止使用內部庫。從遷移中,我們了解到,既然已經引入了模塊,就可以使用開箱即用的標準庫,從而降低了我們服務的維護成本。主要的挑戰是使用 Makefile 微調開發人員環境變量以在開發人員機器上運行(使垃圾收集器工作)。

          改編自testcontainers 文檔的示例:

          ctx :=context.TODO()
          redisContainer, err :=redis.RunContainer(ctx,
              testcontainers.WithImage("docker.io/redis:latest"),
          )
          if err !=nil {
              panic(err)
          }
          defer func() {
              if err :=redisContainer.Terminate(ctx); err !=nil {
                  panic(err)
              }
          }()
          

          結論

          Testcontainers for Golang 是一個很棒的支持測試的庫,現在引入了模塊,它變得更好了。垃圾收集器存在一些小障礙,但可以按照本文所述輕松修復。

          我希望通過這個博客,如果您還沒有采用 Testcontainers,我們強烈推薦它來提高您的應用程序的可測試性。


          作者:Fabien Pozzobon

          出處:https://engineering.zalando.com/posts/2023/12/using-modules-for-testcontainers-with-golang.html

          次聊到了《Go語言進階之路(八):正則表達式


          主站蜘蛛池模板: 成人无码一区二区三区| 精品视频在线观看一区二区三区| 中文字幕视频一区| 国产免费一区二区三区VR| 亚洲一区二区三区高清| 精品一区二区高清在线观看| 日韩一区二区电影| 一区二区三区在线播放| 日本一区免费电影| 亚洲欧美一区二区三区日产| 一区二区中文字幕| 国产成人一区二区三区视频免费| 国产亚洲综合一区二区三区 | AV天堂午夜精品一区| 欧美一区内射最近更新| 久久久久人妻一区二区三区| 国产一区二区三区久久精品| 久99精品视频在线观看婷亚洲片国产一区一级在线| 精品日产一区二区三区手机| 在线精品国产一区二区三区 | 亚洲男女一区二区三区| 无码人妻精品一区二区三| 精品一区二区三区中文字幕| 亚洲AV日韩精品一区二区三区| 日本韩国一区二区三区| 色婷婷香蕉在线一区二区| 亚洲国产欧美国产综合一区 | 国产成人无码精品一区不卡| 国产一区二区不卡老阿姨| 国产未成女一区二区三区| 国产乱码精品一区二区三区四川人 | 日本精品夜色视频一区二区| 色老板在线视频一区二区 | 人妻体内射精一区二区三四| 午夜影视日本亚洲欧洲精品一区| 鲁丝片一区二区三区免费| 国产熟女一区二区三区四区五区| 精品一区二区三区免费视频| 亚洲乱色熟女一区二区三区丝袜| 伊人色综合一区二区三区| 国产精品一区二区久久不卡|