Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
o-實(shí)現(xiàn)一個(gè)簡單的DSL
DSL 是 Domain Specific Language 的縮寫,中文翻譯為領(lǐng)域特定語言(下簡稱 DSL);而與 DSL 相對的就是 GPL,這里的 GPL 并不是我們知道的開源許可證,而是 General Purpose Language 的簡稱,即通用編程語言,也就是我們非常熟悉的 Objective-C、Java、Python 以及 C 語言等等。
簡單說,就是為了解決某一類任務(wù)而專門設(shè)計(jì)的計(jì)算機(jī)語言。
沒有計(jì)算和執(zhí)行的概念;
實(shí)現(xiàn)DSL總共需要完成兩部分工作:
設(shè)計(jì)語法和語義,定義 DSL 中的元素是什么樣的,元素代表什么意思 實(shí)現(xiàn) parser,對 DSL 解析,最終通過解釋器來執(zhí)行 那么我們可以得到DSL的設(shè)計(jì)原則:
大部分編譯器的工作可以被分解為三個(gè)主要階段:解析(Parsing),轉(zhuǎn)化(Transformation)以及 代碼生成(Code Generation)
那么想要實(shí)現(xiàn)一個(gè)腳本解釋器的話,就需要實(shí)現(xiàn)上面的三個(gè)步驟,而且我們發(fā)現(xiàn),承上啟下的是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)
}
得到的結(jié)果:
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 }
并且,作為一個(gè)嵌入式的DSL,我們的設(shè)計(jì)是依托在golang代碼之上運(yùn)行的,我們不需要代碼生成這一個(gè)步驟,直接使用golang來解析AST來執(zhí)行相應(yīng)的操作
那么,我們的現(xiàn)在的工作就是如何解析AST并做相應(yīng)的操作即可.
那么AST是什么結(jié)構(gòu)呢,他大致可以分為如下結(jié)構(gòu)
All declaration nodes implement the Decl interface.
var a int //GenDecl
func main() //FuncDecl
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
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結(jié)構(gòu)我們知道,一個(gè)ast.Decl是由多個(gè)ast.Stmt,并且一個(gè)ast.Stmt是由多個(gè)ast.Expr組成的,簡單來說就是一個(gè)樹形結(jié)構(gòu),那么這么一來就好辦了,代碼大框架一定是遞歸。
我們自底向上,分別實(shí)現(xiàn)對各種類型的ast.Expr,ast.Stmt, ast.Decl的解釋執(zhí)行方法,并把解釋結(jié)果向上傳遞。然后通過一個(gè)根節(jié)點(diǎn)切入,遞歸方式從上向下解釋執(zhí)行即可
主要代碼:
//編譯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: //二元表達(dá)式
ret=this.CompileBinaryExpr(dct, rct, r)
case *ast.CompositeLit: //集合類型
switch r.Type.(type) {
case *ast.ArrayType: //數(shù)組
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:
//賦值在本節(jié)點(diǎn)的內(nèi)存中
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 ")
}
}
代碼的整體結(jié)構(gòu)有了,那么對于DSL中聲明的變量存儲(chǔ),以及局部變量的作用域怎么解決呢
首先,從虛擬內(nèi)存的結(jié)構(gòu)我們得到啟發(fā),可以使用hash表的結(jié)構(gòu)來模擬最基本的內(nèi)存空間以及存取操作,得益于golang的interface{},我們可以把不同數(shù)據(jù)類型的數(shù)據(jù)存入一個(gè)map[string]interface{}中得到一個(gè)范類型的數(shù)組,這樣我們就構(gòu)建出了一個(gè)簡單的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
}
//設(shè)置值
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)
}
那么,如何實(shí)現(xiàn)局部變量的作用域呢?
package main
func main() {
a :=2
for i:=0;i<10;i++ {
a++
b :=2
}
a=3
b=3 //error b的聲明是在for語句中,外部是無法訪問的
}
那么,這個(gè)runtime context的位置就很重要,我們做如下處理:
每個(gè)Stmt節(jié)點(diǎn)都有一個(gè)runtime context 寫入數(shù)據(jù)時(shí),AssignStmt類型在本Stmt節(jié)點(diǎn)中賦值,其他類型新建一個(gè)Stmt子節(jié)點(diǎn)執(zhí)行 讀取數(shù)據(jù)時(shí),從本節(jié)點(diǎn)開始向上遍歷父節(jié)點(diǎn),在runtime context中尋找變量,找到即止 通過這一機(jī)制,我們可以得到的效果是:
同一個(gè)BlockStmt下的多個(gè)Stmt(IfStmt,F(xiàn)orStmt等)處理節(jié)點(diǎn)之間的runtime context是互相隔離的 每個(gè)子節(jié)點(diǎn),都能訪問到父節(jié)點(diǎn)中定義的變量
代碼實(shí)現(xiàn):
type Stmt struct{
Rct *runCxt.RunCxt //變量作用空間
Type int
Father *Stmt //子節(jié)點(diǎn)可以訪問到父節(jié)點(diǎn)的內(nèi)存空間
}
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:
//賦值在本節(jié)點(diǎn)的內(nèi)存中
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系統(tǒng),為了和外部系統(tǒng)保持一個(gè)很好地?cái)?shù)據(jù)類型交互以及數(shù)據(jù)的準(zhǔn)確性,DSL最好也是強(qiáng)類型語言。但是為了簡單,我們會(huì)刪減一些數(shù)據(jù)類型,保留最基本且最穩(wěn)定的數(shù)據(jù)類型
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:
//拼接結(jié)構(gòu)體
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數(shù)據(jù)與go數(shù)據(jù)類型對應(yīng)關(guān)系為:
DSL數(shù)據(jù)類型go數(shù)據(jù)類型備注intint64最大范圍floatfloat64最大范圍stringstringmapmap[interface{}]interface{}最大容忍度array slice[]interface{}{}最大容忍度
通過JsonMap與外部系統(tǒng)進(jìn)行交互,且提供Get(path) Set(path)方法,去動(dòng)態(tài)的訪問與修改Json context中的節(jié)點(diǎn)
但是外部交互Json又是多種結(jié)構(gòu)類型的,借助于nodejson可以解析動(dòng)態(tài)json結(jié)構(gòu),通過XX.X格式的路徑,來動(dòng)態(tài)的訪問和修改json中的字段
解析CallExpr,通過reflect來調(diào)用內(nèi)部函數(shù)
func (this *Expr) CompileCallExpr(dct *dslCxt.DslCxt, rct *Stmt, r *ast.CallExpr) interface{} {
var ret interface{}
//校驗(yàn)內(nèi)置函數(shù)
var funcArgs []reflect.Value
funcName :=r.Fun.(*ast.Ident).Name
//初始化入?yún)?
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使開發(fā)人員能夠輕松地針對容器化依賴項(xiàng)運(yùn)行測試。在我們之前的文章中,您可以找到使用 Testcontainers 進(jìn)行集成測試的介紹,并探索如何使用 Testcontainers(用 Java)編寫功能測試。
這篇博文將深入探討如何使用模塊以及 Golang 測試容器的常見問題。
服務(wù)經(jīng)常使用外部依賴項(xiàng),如數(shù)據(jù)存儲(chǔ)或隊(duì)列。可以模擬這些依賴項(xiàng),但如果您想要運(yùn)行集成測試,最好根據(jù)實(shí)際依賴項(xiàng)(或足夠接近)進(jìn)行驗(yàn)證。
使用依賴項(xiàng)的映像啟動(dòng)容器是驗(yàn)證應(yīng)用程序是否按預(yù)期運(yùn)行的便捷方法。使用 Testcontainers,啟動(dòng)容器是通過編程方式完成的,因此您可以將其定義為測試的一部分。運(yùn)行測試的機(jī)器(開發(fā)人員、CI/CD)需要具有容器運(yùn)行時(shí)接口(例如 Docker、Podman...)
Testcontainers for Go 非常易于使用,快速啟動(dòng)示例如下:
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)
}
}()
如果我們深入研究上面的代碼,我們會(huì)注意到:
從上一節(jié)的例子來看,存在一些小小的不便:
運(yùn)行 Redis 容器可能還需要一些額外的環(huán)境變量和其他參數(shù),這需要更深入的知識(shí)。因此,我們決定創(chuàng)建一個(gè)內(nèi)部庫,該庫將使用簡化測試實(shí)施所需的默認(rèn)參數(shù)初始化容器。為了保持靈活性,我們使用了功能選項(xiàng)模式,以便消費(fèi)者仍然可以根據(jù)需要進(jìn)行自定義。
Redis 的實(shí)現(xiàn)示例:
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)
}
}()
有了這個(gè)內(nèi)部庫,開發(fā)人員可以輕松地為 Redis 添加測試,而無需弄清楚等待策略、暴露端口等。如果出現(xiàn)不兼容的情況,可以更新內(nèi)部庫以集中修復(fù)問題。
Testcontainers 還額外確保了測試完成后容器會(huì)被移除,它使用垃圾收集器defer,這是一個(gè)作為“sidecar”啟動(dòng)的附加容器。即使測試崩潰(這將阻止運(yùn)行),此容器也會(huì)負(fù)責(zé)停止正在測試的容器。
當(dāng)使用Docker時(shí),它可以正常工作,但使用其他容器運(yùn)行時(shí)接口(如Podman)時(shí)經(jīng)常會(huì)遇到這種錯(cuò)誤:Error response from daemon: container create: statfs /var/run/docker.sock: permission denied: creating reaper failed: failed to create container。
“修復(fù)此問題”的一種方法是使用環(huán)境變量將其停用TESTCONTAINERS_RYUK_DISABLED=true。
另一種方法是設(shè)置 Podman 機(jī)器 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
在我們的內(nèi)部庫中,我們采取默認(rèn)禁用它的方法,因?yàn)殚_發(fā)人員在本地運(yùn)行它時(shí)遇到了問題。
一旦我們的內(nèi)部庫足夠穩(wěn)定,我們就決定是時(shí)候通過為 Testcontainers 做貢獻(xiàn)來回饋社區(qū)了。但令人驚訝的是…… Testcontainers 剛剛引入了模塊。模塊的功能與我們的內(nèi)部庫完全一樣,因此我們將所有服務(wù)遷移到模塊并停止使用內(nèi)部庫。從遷移中,我們了解到,既然已經(jīng)引入了模塊,就可以使用開箱即用的標(biāo)準(zhǔn)庫,從而降低了我們服務(wù)的維護(hù)成本。主要的挑戰(zhàn)是使用 Makefile 微調(diào)開發(fā)人員環(huán)境變量以在開發(fā)人員機(jī)器上運(yùn)行(使垃圾收集器工作)。
改編自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 是一個(gè)很棒的支持測試的庫,現(xiàn)在引入了模塊,它變得更好了。垃圾收集器存在一些小障礙,但可以按照本文所述輕松修復(fù)。
我希望通過這個(gè)博客,如果您還沒有采用 Testcontainers,我們強(qiáng)烈推薦它來提高您的應(yīng)用程序的可測試性。
作者:Fabien Pozzobon
出處:https://engineering.zalando.com/posts/2023/12/using-modules-for-testcontainers-with-golang.html
次聊到了《Go語言進(jìn)階之路(八):正則表達(dá)式
*請認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。