使用 Yaegi 在 Go 语言项目中嵌入 Go 作为脚本
背景
目前(2023年2月)维护着公司的对象存储系统,需要一套用于多机房流量调度与缓存的系统。目前由若干套自运维的 NGINX+ATS 集群实现,由于年久失修,存在一些问题,比如:日志信息不足、监控缺失、配置管理困难等,因此需要重构。
理论上可以研究研究 OpenResty 等基于 NGINX 的扩展方案,但由于我们不想为这个项目投入长期人力,能够较为简单的对接公司监控、日志等基础设施,并承载部分 dirty 的业务逻辑,因此决定使用 Go 语言开发。
开发初期,使用 YAML 描述转发规则,但随着规则的增多,发现由于建模不当,规则文件越发冗长繁琐;受限于领域知识的匮乏,难以设计一个简单灵活的 DSL,因此决定使用脚本语言描述转发规则。
尝试了 Go 生态中的 Lua、JS、Python 等方案,但考虑到开发体验和团队成员的技术背景,对于长期维护来说,有些不好接受。恰巧发现了 Yaegi 这个项目,它可以将 Go 语言作为脚本语言嵌入到其他 Go 项目中,并能较为简单的调用宿主程序代码,IDE 体验良好,不需要团队成员学习新的语言。
上手
Yaegi 缺少对外部使用者的文档支持,可能因为它主要是为 Traefik 项目开发,因此在使用过程中遇到了一些问题,这里记录一下。
注册符号表时,导入路径最后一项是包名
官方文档示例代码有使用 Use(stdlib.Symbols)
方法导入符号表,但当我尝试导入自己项目的符号到解释器环境中时,却总是报错,最后在这篇文章里发现原因:
这里需要注意的是:Use 函数传入的 map,其 key 并不是 package 的名称,而是 package 路径 + package 名称的组合。比如说引入一个 package,路径是: github.com/A/B,那么它的 package 路径就是 “github.com/A/B”,package 名称是 B,连在一起的 key 就是: github.com/A/B/B,注意后面被重复了两次的 “B” —— 笔者就被这坑过,卡了好几天。
符号表生成时循环依赖
Yaegi 文档中没提到的,它还提供了一个代码生成工具,用于生成指定 package 的符号表,以下是我使用过程中遇到的坑。
创建一个名为 learn_yaegi
的项目,目录结构如下:
.
├── go.mod
├── go.sum
├── main.go
└── pkg
├── a
│ ├── a.go
└── b
├── b.go
我尝试为 a
包生成符号表,于是创建文件 ./pkg/a/symbols.go
,内容如下:
//go:generate go install github.com/traefik/yaegi/cmd/[email protected]
//go:generate yaegi extract learn_yaegi/a
package a
然后执行 go generate ./...
,生成文件 ./pkg/a/learn_yaegi-a.go
,内容如下:
// Code generated by 'yaegi extract learn_yaegi/a'. DO NOT EDIT.
//go:build go1.20
// +build go1.20
package a
import (
"learn_yaegi/a"
"reflect"
)
func init() {
Symbols["learn_yaegi/a/a"] = map[string]reflect.Value{
// function, constant and variable definitions
"Foo": reflect.ValueOf(a.Foo),
}
}
生成的代码无法通过编译,因为 learn_yaegi-a.go
中又导入了 learn_yaegi/a
包,会导致循环依赖。
实际项目中引用关系更复杂,最后我摸索的实现方式是:创建独立的 package 用于生成符号表,然后在解释器调用处引入该 package 中的 Symbols 变量,目录结构如下:
.
├── go.mod
├── go.sum
├── main.go
├── pkg
│ ├── a
│ │ ├── a.go
│ └── b
│ ├── b.go
└── symbols
└── symbols.go
其中,./symbols/symbols.go
的文件内容如下:
//go:generate go install github.com/traefik/yaegi/cmd/[email protected]
//go:generate yaegi extract learn_yaegi/pkg/a
//go:generate yaegi extract learn_yaegi/pkg/b
package symbols
import "reflect"
var Symbols = map[string]map[string]reflect.Value{}
PS:写了个简单的 Demo 项目,提供了一个相对完整的示范,用于演示 Yaegi 的实际使用方式,见DCjanus/yaegi_demo