导语

Go 作为一种编译型语言,常常用于实现后盾服务的开发。因为 Go 初始的开发大佬都是 C 的老牌使用者,因而 Go 中保留了不少 C 的编程习惯和思维,这对 C/C++ 和 PHP 开发者来说十分有吸引力。作为编译型语言的个性,也让 Go 在多协程环境下的性能有不俗的体现。

但脚本语言则简直都是解释型语言,那么 Go 怎么就和脚本扯上关系了?请读者带着这个疑难,“听” 本文给你娓娓道来~~

本文章采纳 常识共享署名-非商业性应用-雷同形式共享 4.0 国内许可协定 进行许可。

什么样的语言能够作为脚本语言?

程序员们都晓得,高级程序语言从运行原理的角度来说能够分成两种:编译型语言、解释型语言。Go 就是一个典型的编译型语言。

  • 编译型语言就是须要应用编译器,在程序运行之前将代码编译成操作系统可能间接辨认的机器码文件。运行时,操作系统间接拉起该文件,在 CPU 中间接运行
  • 解释型语言则是在代码运行之前,须要先拉起一个解释程序,应用这个程序在运行时就能够依据代码的逻辑执行
汇编语言、C、C++、Objective-C、Go、Rust
JavaScript、PHP、Shell、Python、Lua
Java
PHPJS

能够看到,解释型语言天生适宜作为脚本语言,因为它们本来就须要应用运行时来解释和运行代码。将运行时稍作革新或封装,就能够实现一个动静拉起脚本的性能。

然而,程序员们并不信邪,ta们素来就没有放弃把编译型语言变成脚本语言的致力。

为什么须要用 Go 写脚本?

首先答复一个问题:为什么咱们须要嵌入脚本语言?答案很简略,编译好的程序逻辑曾经固定下来了,这个时候,咱们须要增加一个能力,可能在运行时调整某些局部的性能逻辑,实现这些性能的灵便配置。

yaegi
1.161.17struct

能够看到,yaegi 的三个劣势中,都有 “简” 字。便于上手、便于对接,就是它最大的劣势。

疾速上手

这里,咱们写一段最简略的代码,代码的性能是斐波那契数:

package plugin

func Fib(n int) int {
    return fib(n, 0, 1)
}

func fib(n, a, b int) int {
    if n == 0 {
        return a
    } else if n == 1 {
        return b
    }
    return fib(n-1, b, a+b)
}
const src = ...
package main 

import (
    "fmt"

    "github.com/traefik/yaegi/interp"
    "github.com/traefik/yaegi/stdlib"
)

func main() {
    intp := interp.New(interp.Options{})  // 初始化一个 yaegi 解释器
    intp.Use(stdlib.Symbols)  // 容许脚本调用(简直)所有的 Go 官网 package 代码

    intp.Eval(src)  // src 就是下面的 Go 代码字符串
    v, _ := intp.Eval("plugin.Fib")
    fu := v.Interface().(func(int) int)

    fmt.Println("Fib(35) =", fu(35))
}

// Output:
// Fib(35) = 9227465

const src = `
package plugin

func Fib(n int) int {
    return fib(n, 0, 1)
}

func fib(n, a, b int) int {
    if n == 0 {
        return a
    } else if n == 1 {
        return b
    }
    return fib(n-1, b, a+b)
}`
fu

从这一点来说就显得十分十分的敌对,这意味着运行时,和脚本之间能够间接传递参数,而不须要两头转换。

自定义数据结构传递

前文说到,yaegi 的一个极大的劣势,是能够间接传递自定义 struct 格局。

这里,我先抛出如何传递自定义数据结构的办法,而后再更进一步讲 yaegi 对第三方库的反对。

比如说,我定义了一个自定义的数据结构,并且心愿在 Go 脚本中进行传递:

package slice

// github.com/Andrew-M-C/go.util/slice

// ...

type Route struct {
    XIndexes []int
    YIndexes []int
}

那么,在对 yaegi 解释器进行初始化的时候,咱们能够在 intp 变量初始化实现之后,调用以下代码进行符号表的初始化:

    intp := interp.New(interp.Options{})

    intp.Use(stdlib.Symbols)
    intp.Use(map[string]map[string]reflect.Value{
        "github.com/Andrew-M-C/go.util/slice/slice": {
            "Route": reflect.ValueOf((*slice.Route)(nil)),
        },
    })
github.com/Andrew-M-C/go.util/sliceRoute
Usegithub.com/A/Bgithub.com/A/BBgithub.com/A/B/BB
Yaegi 反对第三方库

原理

intp.Use(stdlib.Symbols)
stdlib.Symbols
Useyaegi

当然,这种办法只能对脚本所能援用的第三方库进行事后定义,而不反对在脚本中动静加载未定义的第三方库。即便如此,这也极大地扩大了 yaegi 脚本的性能。

符号解析

yaegi
go generate
github.com/Andrew-M-C/go.util/slice
//go:generate go install github.com/traefik/yaegi/cmd/yaegi@v0.10.0
//go:generate yaegi extract github.com/Andrew-M-C/go.util/slice
github_com-Andrew-M-C-go_util-slice.go
与其余脚本计划的比照

性能比照

咱们在调研了 yaegi 之外,也另外调研和比照了 tengo 和应用 Lua 的 gopher-lua。其中后者也是团队利用得比拟成熟的库。

笔者须要特别强调的是:tengo 的题目尽管说本人用的是 Go,但实际上是挂羊头卖狗肉。它应用是本人的一套独立语法,与官网 Go 齐全不兼容,甚至乎连类似都称不上。咱们该当把它当作另一种脚本语言来看。

这三种计划的比照如下:

table

总而言之:

  • gopher 的劣势在于性能
  • yaegi 的劣势在于 Go 原生语法,以及能够承受的性能
  • tengo 的劣势?对于笔者的这一应用场景来说,不存在的

然而 yaegi 也有很显著的有余:

0.y.z

性能比照

下文的表格比拟多,这里先抛这三个库的比照论断吧:

  • 从纯算力性能上看,gopher 领有压倒性的劣势
  • yaegi 的性能很稳固,大概是 gopher 的 1/5 ~ 1/4 之间
  • 非计算密集型的场景下,tengo 的性能比拟蹩脚。均匀场景也是最差的

简略的 a + b

res := a + b
包名 脚本语言 每迭代耗时 内存占用 alloc数
Go 原生 Go 1.352 ns 0 B 0
yaegi Go 687.8 ns 352 B 9
tengo tengo 19696 ns 90186 B 6
gopher lua 171.2 ns 40 B 2

后果让人大跌眼镜,对于特地简略的脚本,tengo 的耗时极高,很可能是在进入和退出 tengo VM 时,耗费了过多的资源。
而 gopher 则体现出了优异的性能。让人印象十分粗浅。

条件判断

该逻辑也很简略,判断输出数是否大于零。测试后果与简略加法相似,如下:

包名 脚本语言 每迭代耗时 内存占用 alloc数
Go 原生 Go 1.250 ns 0 B 0
yaegi Go 583.1 ns 280 B 7
tengo tengo 18195 ns 90161 B 3
gopher Lua 116.2 ns 8 B 1

斐波那契数

Fib
包名 脚本语言 每迭代耗时 内存占用 alloc数
Go 原生 Go 104.6 ns 0 B 0
yaegi Go 21091 ns 14680 B 321
tengo tengo 25259 ns 90714 B 73
gopher Lua 5042 ns 594 B 1

这么说吧:tengo 号称与原生 Go 相当,然而实际上整整差了两个数量级,并且还是这几个竞争者之间的性能是最低的。

这个测试后果与 tengo 的 README 上声称的 benchmark 数据出入也很大,如果读者晓得 tengo 的测试方法是什么,或者是我的测试方法哪里有问题,也心愿不吝指出~~

工程利用留神要点
stdlib.Symbols
os/xxxnet/xxxlogio/xxxdatabase/xxxruntime

此外,尽管 yaegi 间接将脚本函数裸露进去能够间接调用,然而主程序不能对脚本的可靠性做任何的假如。换句话说,脚本可能会 panic,或者是批改了主程序的变量,从而导致主程序 panic。为了防止这一点,咱们要将脚本放在一个受限的环境里运行,除了后面通过限度 yaegi 可调用的 package 的形式之外,还应该限度调用脚本的形式。包含但不限于以下几个伎俩:

recovergo\sgo 

当然,文中充斥了对 tengo 的不推崇,也只是在笔者的这种应用场景下,tengo 没有任何劣势而已,请读者辩证浏览,也欢送补充和斧正~~

本文章采纳 常识共享署名-非商业性应用-雷同形式共享 4.0 国内许可协定 进行许可。

原文题目:《Yaegi,让你用规范 Go 语法开发可热插拔的脚本和插件》

公布日期:2021-10-20

原文链接:https://cloud.tencent.com/developer/article/1890816。