Go&lua——github.com/yuin/gopher-lua

目录

go调用lua

这里比较下两个比较有名的go-lua包:

github.com/Shopify/go-luagithub.com/yuin/gopher-lua是两个Go语言库,允许Go程序与Lua脚本进行交互。

以下是这两个库之间的主要区别:

  1. Shopify/go-lua:

    • Shopify/go-lua是一个用Go编写的Lua解释器。
    • 它旨在提供一个轻量级、易于使用的Go和Lua之间的接口。
    • 该项目主要关注简单性和易集成性。
    • 它提供了调用Go函数和Lua函数之间的绑定。
  2. yuin/gopher-lua:

    • yuin/gopher-lua是一个更丰富功能且活跃维护的Go语言Lua虚拟机实现。
    • 它支持广泛的Lua功能,并包含一个标准库,涵盖许多常见的Lua功能。
    • 这个库允许Go代码执行Lua脚本,访问Lua变量,并注册Go函数供Lua脚本使用。
    • 它提供了更全面的文档和更大的社区,相比之下优于github.com/Shopify/go-lua
    • 根据我上次更新的信息,yuin/gopher-lua已经有了更近期的更新和改进,这表明它的维护活跃性更高。

在选择这些库时,请考虑项目的具体要求。如果您需要与Lua进行简单轻量级集成,github.com/Shopify/go-lua可能已经足够。另一方面,如果您需要一个功能更丰富且持续维护的库,支持更广泛的Lua功能,github.com/yuin/gopher-lua会是一个更好的选择。

安装

GopherLua supports >= Go1.9.

bash 复制代码
go get github.com/yuin/gopher-lua

使用

GopherLua的API与Lua的运行方式非常相似,但是堆栈仅用于传递参数和接收返回值。

Run scripts in the VM.

go 复制代码
L := lua.NewState()
defer L.Close()
// 直接执行lua代码
if err := L.DoString(`print("hello")`); err != nil {
    panic(err)
}
go 复制代码
L := lua.NewState()
defer L.Close()
// 执行lua脚本文件
if err := L.DoFile("hello.lua"); err != nil {
    panic(err)
}

调用栈和注册表大小:

LState的调用栈大小控制脚本中Lua函数的最大调用深度(Go函数调用不计入其中)。

LState的注册表实现对调用函数(包括Lua和Go函数)的栈式存储,并用于表达式中的临时变量。其存储需求会随着调用栈的使用和代码复杂性的增加而增加。

注册表和调用栈都可以设置为固定大小或自动大小。

当您在进程中实例化大量的LState时,值得花时间来调整注册表和调用栈的选项。

注册表

注册表可以在每个LState的基础上配置初始大小、最大大小和步长大小。这将允许注册表根据需要进行扩展。一旦扩展,它将不会再缩小。

go 复制代码
 L := lua.NewState(lua.Options{
    RegistrySize: 1024 * 20,         // this is the initial size of the registry
    RegistryMaxSize: 1024 * 80,      // this is the maximum size that the registry can grow to. If set to `0` (the default) then the registry will not auto grow
    RegistryGrowStep: 32,            // this is how much to step up the registry by each time it runs out of space. The default is `32`.
 })
defer L.Close()

如果注册表对于给定的脚本来说太小,最终可能会导致程序崩溃。而如果注册表太大,将会浪费内存(如果实例化了许多LState,这可能是一个显著的问题)。自动增长的注册表在调整大小时会带来一点点性能损耗,但不会影响其他方面的性能。

调用栈

调用栈可以以两种不同的模式运行,即固定大小或自动大小。固定大小的调用栈具有最高的性能,并且具有固定的内存开销。自动大小的调用栈将根据需要分配和释放调用栈页面,从而确保任何时候使用的内存量最小。缺点是,每次分配新的调用帧页面时都会带来一点小的性能影响。默认情况下,一个LState会以每页8个调用帧的方式分配和释放调用栈帧,因此不会在每个函数调用时产生额外的分配开销。对于大多数用例,自动调整大小的调用栈的性能影响可能是可以忽略的。

go 复制代码
 L := lua.NewState(lua.Options{
     CallStackSize: 120,                 // this is the maximum callstack size of this LState
     MinimizeStackMemory: true,          // Defaults to `false` if not specified. If set, the callstack will auto grow and shrink as needed up to a max of `CallStackSize`. If not set, the callstack will be fixed at `CallStackSize`.
 })
defer L.Close()

Data model

在GopherLua程序中,所有的数据都是LValue。LValue是一个接口类型,具有以下方法:

go 复制代码
type LValue interface {
	String() string
	Type() LValueType
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
	assertFloat64() (float64, bool)
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
	assertString() (string, bool)
	// to reduce `runtime.assertI2T2` costs, this method should be used instead of the type assertion in heavy paths(typically inside the VM).
	assertFunction() (*LFunction, bool)
}

实现LValue接口的对象是:

Type name Go type Type() value Constants
LNilType (constants) LTNil LNil
LBool (constants) LTBool LTrue, LFalse
LNumber float64 LTNumber -
LString string LTString -
LFunction struct pointer LTFunction -
LUserData struct pointer LTUserData -
LState struct pointer LTThread -
LTable struct pointer LTTable -
LChannel chan LValue LTChannel -

Go中调用lua

go 复制代码
L := lua.NewState()
defer L.Close()
// 加载double.lua
if err := L.DoFile("double.lua"); err != nil {
    panic(err)
}

if err := L.CallByParam(lua.P{
	// 获取函数名
    Fn: L.GetGlobal("double"),
    NRet: 1,
    Protect: true,
    }, lua.LNumber(10)); err != nil {
    panic(err)
}
ret := L.Get(-1) // returned value
L.Pop(1)  // remove received value

Lua支持多个参数和多个返回值,参数好办,用lua.LNumber(123),返回值个数也可以是多个,调用CallByParam的时候,NRet就是返回参数个数,Fn是要调用的全局函数名,Protect为true时,如果没找到函数或者出错不会panic,只会返回err。

**GopherLua的函数调用是通过堆栈来进行的,调用前将参数压栈,完事后将结果放入堆栈中,调用方在堆栈顶部拿结果。**调用完成后,要以压栈的方式,一个一个取回返回值ret := L.Get(-1)。

fib.lua 脚本内容:

lua 复制代码
function fib(n)
    if n < 2 then return n end
    return fib(n-1) + fib(n-2)
end
go 复制代码
package main
 
import (
	"fmt"
	lua "github.com/yuin/gopher-lua"
)
 
func main() {
	// 1、创建 lua 的虚拟机
	L := lua.NewState()
	defer L.Close()
	// 加载fib.lua
	// Calling DoFile will load a Lua script, compile it to byte code and run the byte code in a LState.
	// 所以DoFile既加载了文件又执行了字节码
	if err := L.DoFile(`fib.lua`); err != nil {
		panic(err)
	}
	// 调用fib(n)
	err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("fib"), // 获取fib函数引用
		NRet:    1,                  // 指定返回值数量
		Protect: true,               // 如果出现异常,是panic还是返回err
	}, lua.LNumber(10)) // 传递输入参数n
	if err != nil {
		panic(err)
	}
	// 获取返回结果
	ret := L.Get(-1)
	// 从堆栈中扔掉返回结果
    // 这里一定要注意,不调用此方法,后续再调用 L.Get(-1) 获取的还是上一次执行的结果
    // 这里大家可以自己测试下
	L.Pop(1)
	// 打印结果
	res, ok := ret.(lua.LNumber)
	if ok {
		fmt.Println(int(res))
	} else {
		fmt.Println("unexpected result")
	}
}

API

Lua调用go

LGFunction类型:type LGFunction func(*LState) int

go 复制代码
func main() {
	L := lua.NewState()
	defer L.Close()
	L.SetGlobal("double", L.NewFunction(func(state *lua.LState) int {
		lv := state.ToInt(1)        /* get argument */
		L.Push(lua.LNumber(lv * 2)) /* push result */
		// 返回值个数
		return 1
	}))
	// 加载编译执行hello.lua
	L.DoFile("./hello.lua")
}

hello.lua中的内容:

lua 复制代码
print(double(100))

再来一个案例:

go 复制代码
package main

import (
	"fmt"

	lua "github.com/yuin/gopher-lua"
)

func Add(L *lua.LState) int {
	// 获取参数
	arg1 := L.ToInt(1)
	arg2 := L.ToInt(2)

	ret := arg1 + arg2

	// 返回值
	L.Push(lua.LNumber(ret))
	// 返回值的个数
	return 1
}

func main() {
	L := lua.NewState()
	defer L.Close()

	// 注册全局函数
	L.SetGlobal("add", L.NewFunction(Add))

	// go
	err := L.DoFile("main.lua")
	if err != nil {
		fmt.Print(err.Error())
		return
	}
}

main.lua内容:

lua 复制代码
print(add(10,20))

打开Lua内置模块的子集

打开Lua内置模块的子集可以通过以下方式实现,例如,可以避免启用具有访问本地文件或系统调用权限的模块。

go 复制代码
func main() {
    L := lua.NewState(lua.Options{SkipOpenLibs: true})
    defer L.Close()
    for _, pair := range []struct {
        n string
        f lua.LGFunction
    }{
        {lua.LoadLibName, lua.OpenPackage}, // Must be first
        {lua.BaseLibName, lua.OpenBase},
        {lua.TabLibName, lua.OpenTable},
    } {
        if err := L.CallByParam(lua.P{
            Fn:      L.NewFunction(pair.f),
            NRet:    0,
            Protect: true,
        }, lua.LString(pair.n)); err != nil {
            panic(err)
        }
    }
    if err := L.DoFile("main.lua"); err != nil {
        panic(err)
    }
}

使用Go创建模块

GopherLua除了可以满足基本的lua需要,还将Go语言特有的高级设计直接移植到lua环境中,使得内嵌的脚本也具备了一些高级的特性

可以使用context.WithTimeout对执行的lua脚本进行超时

可以使用context.WithCancel打断正在执行的lua脚本

多个lua解释器实例之间还可以通过channel共享数据

支持多路复用选择器select

使用Lua作为内嵌脚本的另外一个重要优势在于Lua非常轻量级,占用内存极小。

示例1(官方)

mymodule.go:

go 复制代码
package main

import lua "github.com/yuin/gopher-lua"

var exports = map[string]lua.LGFunction{
	"myfunc": myfunc,
}

func myfunc(L *lua.LState) int {
	return 0
}

func Loader(L *lua.LState) int {
	// register functions to the table
	mod := L.SetFuncs(L.NewTable(), exports)
	// 注册name属性到module
	L.SetField(mod, "name", lua.LString("value"))

	// returns the module
	L.Push(mod)
	return 1
}

mymain.go:

go 复制代码
package main

import (
    "./mymodule"
    "github.com/yuin/gopher-lua"
)

func main() {
    L := lua.NewState()
    defer L.Close()
    L.PreloadModule("mymodule", mymodule.Loader)
    if err := L.DoFile("main.lua"); err != nil {
        panic(err)
    }
}

main.lua:

go 复制代码
local m = require("mymodule")
m.myfunc()
print(m.name)

示例2

在Go语言的实现中,当我们将一个函数注册为Lua函数(通过L.SetFuncs等方法),它会被转换为LGFunction类型,这样可以确保与Lua C API兼容。返回的整数值用于指示函数的返回值数量(通常用0表示成功,1表示出错)。

实际上,这个整数值在Go的GopherLua实现中没有特别的意义,因为Go语言不需要遵循C API规范。但为了与标准的Lua C API保持一致,Go的GopherLua库仍然要求注册给Lua的函数遵循这个规范,即返回一个整数值。

因此,在Go的GopherLua中,LGFunction类型一定要有返回值,以满足与Lua C API的兼容性需求,即使这个返回值在Go代码中可能并没有特别的实际意义。

go 复制代码
package main

import (
	lua "github.com/yuin/gopher-lua"
)

var exports = map[string]lua.LGFunction{
	"add": add,
}

func add(L *lua.LState) int {
	// 第一个参数
	firstArg := L.Get(1).(lua.LNumber)
	// 第二个参数
	secondArg := L.Get(2).(lua.LNumber)

	// 返回值
	L.Push(firstArg + secondArg)
	// 返回值个数
	return 1
}

func Loader(L *lua.LState) int {
	// register functions to the table
	mod := L.SetFuncs(L.NewTable(), exports)

	// returns the module
	L.Push(mod)

	return 1
}

关闭一个运行的lua虚拟机

go 复制代码
L := lua.NewState()
defer L.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// set the context to our LState
L.SetContext(ctx)
err := L.DoString(`
  local clock = os.clock
  function sleep(n)  -- seconds
    local t0 = clock()
    while clock() - t0 <= n do end
  end
  sleep(3)
`)
// err.Error() contains "context deadline exceeded"

虚拟机之间共享lua字节码

调用DoFile会加载Lua脚本,将其编译为字节码并在LState中运行该字节码。

如果您有多个LState,都需要运行相同的脚本,您可以在它们之间共享字节码,这将节省内存。共享字节码是安全的,因为它是只读的,不会被Lua脚本更改。

go 复制代码
// CompileLua reads the passed lua file from disk and compiles it.
func CompileLua(filePath string) (*lua.FunctionProto, error) {
    file, err := os.Open(filePath)
    defer file.Close()
    if err != nil {
        return nil, err
    }
    reader := bufio.NewReader(file)
    chunk, err := parse.Parse(reader, filePath)
    if err != nil {
        return nil, err
    }
    proto, err := lua.Compile(chunk, filePath)
    if err != nil {
        return nil, err
    }
    return proto, nil
}

// DoCompiledFile takes a FunctionProto, as returned by CompileLua, and runs it in the LState. It is equivalent
// to calling DoFile on the LState with the original source file.
func DoCompiledFile(L *lua.LState, proto *lua.FunctionProto) error {
    lfunc := L.NewFunctionFromProto(proto)
    L.Push(lfunc)
    return L.PCall(0, lua.MultRet, nil)
}

// Example shows how to share the compiled byte code from a lua script between multiple VMs.
func Example() {
    codeToShare := CompileLua("mylua.lua")
    a := lua.NewState()
    b := lua.NewState()
    c := lua.NewState()
    DoCompiledFile(a, codeToShare)
    DoCompiledFile(b, codeToShare)
    DoCompiledFile(c, codeToShare)
}

go-lua调优

预编译

在查看上述 DoString(...) 方法的调用链后,发现每执行一次 DoString(...) 或 DoFile(...) ,都会各执行一次 parse 和 compile 。

go 复制代码
func (ls *LState) DoString(source string) error {
    if fn, err := ls.LoadString(source); err != nil {
        return err
    } else {
        ls.Push(fn)
        return ls.PCall(0, MultRet, nil)
    }
}

func (ls *LState) LoadString(source string) (*LFunction, error) {
    return ls.Load(strings.NewReader(source), "<string>")
}

func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {
    chunk, err := parse.Parse(reader, name)
    // ...
    proto, err := Compile(chunk, name)
    // ...
}

从这一点考虑,在同份 Lua 代码将被执行多次的场景下,如果我们能够对代码进行提前编译,那么应该能够减少 parse 和 compile 的开销。

go 复制代码
package glua_test

import (
    "bufio"
    "os"
    "strings"

    lua "github.com/yuin/gopher-lua"
    "github.com/yuin/gopher-lua/parse"
)

// 编译 lua 代码字段
func CompileString(source string) (*lua.FunctionProto, error) {
    reader := strings.NewReader(source)
    chunk, err := parse.Parse(reader, source)
    if err != nil {
        return nil, err
    }
    proto, err := lua.Compile(chunk, source)
    if err != nil {
        return nil, err
    }
    return proto, nil
}

// 编译 lua 代码文件
func CompileFile(filePath string) (*lua.FunctionProto, error) {
    file, err := os.Open(filePath)
    defer file.Close()
    if err != nil {
        return nil, err
    }
    reader := bufio.NewReader(file)
    chunk, err := parse.Parse(reader, filePath)
    if err != nil {
        return nil, err
    }
    proto, err := lua.Compile(chunk, filePath)
    if err != nil {
        return nil, err
    }
    return proto, nil
}

func BenchmarkRunWithoutPreCompiling(b *testing.B) {
    l := lua.NewState()
    for i := 0; i < b.N; i++ {
        _ = l.DoString(`a = 1 + 1`)
    }
    l.Close()
}

func BenchmarkRunWithPreCompiling(b *testing.B) {
    l := lua.NewState()
    proto, _ := CompileString(`a = 1 + 1`)
    lfunc := l.NewFunctionFromProto(proto)
    for i := 0; i < b.N; i++ {
        l.Push(lfunc)
        _ = l.PCall(0, lua.MultRet, nil)
    }
    l.Close()
}

// goos: darwin
// goarch: amd64
// pkg: glua
// BenchmarkRunWithoutPreCompiling-8         100000             19392 ns/op           85626 B/op         67 allocs/op
// BenchmarkRunWithPreCompiling-8           1000000              1162 ns/op            2752 B/op          8 allocs/op
// PASS
// ok      glua    3.328s

虚拟机实例池

新建一个 Lua 虚拟机会涉及到大量的内存分配操作,如果采用每次运行都重新创建和销毁的方式的话,将消耗大量的资源。引入虚拟机实例池,能够复用虚拟机,减少不必要的开销。

go 复制代码
type lStatePool struct {
    m     sync.Mutex
    saved []*lua.LState
}

func (pl *lStatePool) Get() *lua.LState {
    pl.m.Lock()
    defer pl.m.Unlock()
    n := len(pl.saved)
    if n == 0 {
        return pl.New()
    }
    x := pl.saved[n-1]
    pl.saved = pl.saved[0 : n-1]
    return x
}

func (pl *lStatePool) New() *lua.LState {
    L := lua.NewState()
    // setting the L up here.
    // load scripts, set global variables, share channels, etc...
    return L
}

func (pl *lStatePool) Put(L *lua.LState) {
    pl.m.Lock()
    defer pl.m.Unlock()
    pl.saved = append(pl.saved, L)
}

func (pl *lStatePool) Shutdown() {
    for _, L := range pl.saved {
        L.Close()
    }
}

// Global LState pool
var luaPool = &lStatePool{
    saved: make([]*lua.LState, 0, 4),
}

README 提供的实例池实现,但注意到该实现在初始状态时,并未创建足够多的虚拟机实例(初始时,实例数为 0),以及存在 slice 的动态扩容问题,这都是值得改进的地方(这是一个可以提交pr的点)。

模块调用

gopher-lua 支持 Lua 调用 Go 模块,在 Golang 程序开发中,我们可能设计出许多常用的模块,这种跨语言调用的机制,使得我们能够对代码、工具进行复用。

go 复制代码
package main

import (
    "fmt"

    lua "github.com/yuin/gopher-lua"
)

const source = `
local m = require("gomodule")
m.goFunc()
print(m.name)
`

func main() {
    L := lua.NewState()
    defer L.Close()
    L.PreloadModule("gomodule", load)
    if err := L.DoString(source); err != nil {
        panic(err)
    }
}

func load(L *lua.LState) int {
    mod := L.SetFuncs(L.NewTable(), exports)
    L.SetField(mod, "name", lua.LString("gomodule"))
    L.Push(mod)
    return 1
}

var exports = map[string]lua.LGFunction{
    "goFunc": goFunc,
}

func goFunc(L *lua.LState) int {
    fmt.Println("golang")
    return 0
}

// golang
// gomodule
相关推荐
量子位1 小时前
字节版龙虾架构火爆GitHub!开源获35k+ Star,内置Skill全家桶,原生适配飞书
github·ai编程
参.商.5 小时前
【Day43】49. 字母异位词分组
leetcode·golang
汪海游龙5 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github
参.商.5 小时前
【Day45】647. 回文子串 5. 最长回文子串
leetcode·golang
HealthScience6 小时前
github怎么授权ssh(私人库授权)
运维·ssh·github
CoderJia程序员甲7 小时前
GitHub 热榜项目 - 日榜(2026-03-22)
人工智能·ai·大模型·github·ai教程
AMoon丶7 小时前
Golang--内存管理
开发语言·后端·算法·缓存·golang·os
阿里嘎多学长7 小时前
2026-03-22 GitHub 热点项目精选
开发语言·程序员·github·代码托管
研究点啥好呢7 小时前
3月22日GitHub热门项目推荐|网页浏览,何须手动
人工智能·python·开源·github