今天群里闲聊的时候,群友想实现动态插件效果


之前写插件,要么是js,要么是python这类动态语言
用go能实现吗?
答案是可以的
突然想到之前用到的一个库,可以实现动态执行go代码
go get github.com/traefik/yaegi
简单分享一下这个库的用法,在应对一些简单场景有奇效
interp.New(options Options) *Interpreter:创建解释器实例
i.Use(values Exports):注入符号(包、函数、变量)到解释器,用于宿主程序向动态代码暴露能力
i.Eval(src string) (res reflect.Value, err error):执行动态代码,返回执行结果(如变量值、函数返回值)和错误
执行代码片段
package main
import (
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func main() {
i := interp.New(interp.Options{})
// 加载标准库(必须加载才能使用 fmt、os 等包)
i.Use(stdlib.Symbols)
// 执行代码
_, err := i.Eval(`
import "fmt"
func add(a, b int) int {
return a + b
}
func main(){
fmt.Println("方式 3 结果:", add(30, 40))
}
`)
if err != nil {
panic(err)
}
}
打包之后也是可以执行的
获取动态代码的变量
通过 i.Eval 获取动态代码中定义的变量或函数,再通过类型断言使用。
package main
import (
"fmt"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func main() {
i := interp.New(interp.Options{})
// 加载标准库(必须加载才能使用 fmt、os 等包)
i.Use(stdlib.Symbols)
// 执行代码
_, err := i.Eval(`
var dynamicVar string = "fengfeng"
func dynamicAdd(a, b int) int {
return a + b
}
`)
if err != nil {
panic(err)
}
// 1. 宿主读取动态变量
dynamicVar, err := i.Eval(`dynamicVar`)
if err != nil {
panic(err)
}
fmt.Println("读取动态变量:", dynamicVar.Interface().(string)) // 输出:读取动态变量:我是动态变量
// 2. 宿主调用动态函数(通过类型断言转为函数类型)
dynamicAddVal, err := i.Eval(`dynamicAdd`)
if err != nil {
panic(err)
}
// 断言为函数类型:func(int, int) int
dynamicAdd := dynamicAddVal.Interface().(func(int, int) int)
res := dynamicAdd(5, 3)
fmt.Println("调用动态函数:", res) // 输出:调用动态函数:8
}
宿主向动态代码交互
在动态代码里面获取宿主里面的变量和函数
package main
import (
"fmt"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
"reflect"
)
func hostGreet(name string) string {
return fmt.Sprintf("宿主程序问候:Hello, %s!", name)
}
var hostVersion = "v1.0.0"
func main() {
// 创建解释器,指定 GoPath 避免警告
i := interp.New(interp.Options{})
// 加载标准库
i.Use(stdlib.Symbols)
i.Use(interp.Exports{
"host/host": map[string]reflect.Value{
"Greet": reflect.ValueOf(hostGreet),
"Version": reflect.ValueOf(hostVersion),
},
})
_, err := i.Eval(`
import (
"fmt"
"host"
)
func main(){
fmt.Println(host.Greet("动态代码用户"));
fmt.Println("宿主版本:", host.Version);
}
`)
if err != nil {
fmt.Printf("执行失败:%v\n", err)
panic(err)
}
}
动态执行go文件
把上面的代码,放到一个文件里面,然后直接加载这个文件也是可以的
package main
import (
"fmt"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func main() {
// 创建解释器,指定 GoPath 避免警告
i := interp.New(interp.Options{})
// 加载标准库
i.Use(stdlib.Symbols)
val, err := i.EvalPath("xxx.go")
fmt.Println(val, err)
}
需要注意的是,不是任何go代码都可以执行,有些go代码是执行不了的
.s不支持程序集文件( )。- 不支持调用 C 代码(没有虚拟“C”包)。
- 不支持有关编译器、链接器或嵌入文件的指令。
- 不能动态添加要从预编译代码中使用的接口,因为需要预编译接口包装器。
- 使用 %T表示类型
reflect和打印值在编译模式和解释模式下可能会产生不同的结果。 - 解释执行计算密集型代码的速度可能比编译执行慢得多。
用之前做下测试,把自己要用的场景都覆盖完,看看能不能行
动态插件案例
其实就是封装api调用这个过程
我的想法就是每个模型创建一个插件
比如qianwen.go,chatgpt.go
在宿主程序里面写完整的逻辑
可以在插件里面重写几个重点方法
宿主程序
package main
import (
"fmt"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
"io"
"net/http"
"reflect"
)
type Config struct {
Url string
}
type Req struct {
Content string
}
func main() {
// 创建解释器,指定 GoPath 避免警告
i := interp.New(interp.Options{})
// 加载标准库
i.Use(stdlib.Symbols)
_, err := i.EvalPath("utils/qianwen.go")
if err != nil {
panic(err)
}
_setModel, err := i.Eval("utils.SetModel")
if err != nil {
// 没有重写utils.SetModel方法,用默认的
return
}
// 重写了setModel,用重写的方法
var config = Config{
Url: "http://xxx.com",
}
_setModel.Call([]reflect.Value{
reflect.ValueOf(&config),
})
fmt.Println("重写之后的配置对象", config)
// 调用do方法
_do, err := i.Eval("utils.Do")
if err != nil {
// 没有重写utils.SetModel方法,用默认的
return
}
var req = Req{
Content: "你好",
}
val := _do.Call([]reflect.Value{
reflect.ValueOf(&req),
})
_response, _err := val[0], val[1]
if !_err.IsNil() {
err = _err.Interface().(error)
if err != nil {
fmt.Println("请求错误", err)
return
}
}
response := _response.Interface().(*http.Response)
byteData, _ := io.ReadAll(response.Body)
fmt.Println("请求响应", string(byteData))
}
插件程序
package utils
import (
"fmt"
"net/http"
"reflect"
)
// SetModel 可以修改配置对象
func SetModel(config any) {
c := reflect.ValueOf(config)
c.Elem().FieldByName("Url").SetString("http://qianwen.com")
fmt.Println("重写SetModel", config)
}
// Do 请求的钩子函数
func Do(req any) (response any, err error) {
fmt.Println("开始请求", req)
response, err = http.Get("https://baidu.com")
//err = errors.New("出错了")
return
}