最近熟悉 go 项目时,发现项目中有用到 github.com/yuin/gopher-lua这个包,之前并没有接触过,特意去看了官方文档和找了些网上的资料,特此记录下。
本次介绍计划分为两篇文章,这一次主要介绍 github.com/yuin/gopher-lua 这个包的介绍以及基础使用,下一边将介绍 github.com/yuin/gopher-lua 是如何在项目中使用的。如有不对的地方,请不吝赐教,谢谢。
文章中的 gopher-lua 如果没有特别说明,即为:github.com/yuin/gopher-lua。
1、 gopher-lua 基础介绍
我们先开看看官方是如何介绍自己的:- GopherLua is a Lua5.1(+ goto statement in Lua5.2) VM and compiler written in Go. GopherLua has a same goal with Lua: Be a scripting language with extensible semantics . It provides Go APIs that allow you to easily embed a scripting language to your Go host programs.
复制代码GopherLua是一个Lua5.1(Lua5.2中的+goto语句)虚拟机和用Go编写的编译器。GopherLua与Lua有着相同的目标:成为一种具有可扩展语义的脚本语言。它提供了Go API,允许您轻松地将脚本语言嵌入到Go主机程序中。
看上面的翻译还是有点抽象,说说自己的理解。 github.com/yuin/gopher-lua 是一个纯 Golang 实现的 Lua 虚拟机,它能够很轻松的在 go 写的程序中调用 lua 脚本。另外提一嘴,使用插件后,也能够在 lua 脚本中调用 go 写好的代码。挺秀的!
接下来我们看一看, github.com/yuin/gopher-lua 的性能如何,这里就直接引用官方自己做的测试来介绍。详情见 wiki page 链接。点进链接过后,发现性能还不错,执行效率和性能仅比 C 实现的 bindings 差点。
官方测试例子是生成斐波那契数列,测试执行结果如下:
progtimeanko182.73sotto173.32sgo-lua8.13sPython3.45.84sGopherLua5.40slua5.1.41.71s2、 gopher-lua 基础介绍
下面的介绍,都是基于 v1.1.0 版本进行的。- go get github.com/yuin/gopher-lua@v1.1.0
复制代码 Go的版本需要 >= 1.9
2.1 gopher-lua 中的 hello world
这里写一个简单的程序,了解 gopher-lua 是如何使用的。- package main
- import (
- lua "github.com/yuin/gopher-lua"
- )
- func main() {
- // 1、创建 lua 的虚拟机
- L := lua.NewState()
- // 执行完毕后关闭虚拟机
- defer L.Close()
- // 2、加载fib.lua
- if err := L.DoString(`print("hello world")`); err != nil {
- panic(err)
- }
- }
复制代码 执行结果:看到这里,感觉没啥特别的地方,接下来,我们看一看 gopher-lua 如何调用事先写好的 lua脚本。
fib.lua 脚本内容:- function fib(n)
- if n < 2 then return n end
- return fib(n-1) + fib(n-2)
- end
复制代码 main.go- package main
- import (
- "fmt"
- lua "github.com/yuin/gopher-lua"
- )
- func main() {
- // 1、创建 lua 的虚拟机
- L := lua.NewState()
- defer L.Close()
- // 加载fib.lua
- 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")
- }
- }
复制代码 执行结果:
55
从上面我们已经能够感受到部分 gopher-lua 的魅力了。接下来,我们就一起详细的学习学习 gopher-lua 。
2.2 gopher-lua 中的数据类型
All data in a GopherLua program is an LValue . LValue is an interface type that has following methods.
GopherLua程序中的所有数据都是一个LValue。LValue是一种具有以下方法的接口类型。
- String() string
- Type() LValueType
- // value.go:29
- 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)
- }
复制代码 上面来自官方的介绍,接下来我们看看 gopher-lua 支持那些数据类型。
Type nameGo typeType() valueConstantsLNilType(constants)LTNilLNilLBool(constants)LTBoolLTrue, LFalseLNumberfloat64LTNumber-LStringstringLTString-LFunctionstruct pointerLTFunction-LUserDatastruct pointerLTUserData-LStatestruct pointerLTThread-LTablestruct pointerLTTable-LChannelchan LValueLTChannel-具体的实现,大家有兴趣,可以自己去看看源码,这里就不做分析了。
那我们是如何知道 go 调用 lua 函数后,得到结果的类型呢?我们可以通过以下方式来知道:- package main
- import (
- "fmt"
- lua "github.com/yuin/gopher-lua"
- )
- func main() {
- // 1、创建 lua 的虚拟机
- L := lua.NewState()
- defer L.Close()
- // 加载fib.lua
- if err := L.DoFile(`fib.lua`); err != nil {
- panic(err)
- }
- TestString(L)
- }
- func TestString(L *lua.LState) {
- err := L.CallByParam(lua.P{
- Fn: L.GetGlobal("TestLString"), // 获取函数引用
- NRet: 1, // 指定返回值数量
- Protect: true, // 如果出现异常,是panic还是返回err
- })
- if err != nil {
- panic(err)
- }
- lv := L.Get(-1) // get the value at the top of the stack
- // 从堆栈中扔掉返回结果
- L.Pop(1)
- if str, ok := lv.(lua.LString); ok {
- // lv is LString
- fmt.Println(string(str))
- }
- if lv.Type() != lua.LTString {
- panic("string required.")
- }
- }
复制代码 fib.lua中的代码:- function TestLString()
- return "this is test"
- end
复制代码 接下来看看指针类型是如何判断的:- lv := L.Get(-1) // get the value at the top of the stack
- if tbl, ok := lv.(*lua.LTable); ok {
- // lv is LTable
- fmt.Println(L.ObjLen(tbl))
- }
复制代码 特别注意:
- LBool , LNumber , LString 这三类不是指针类型,其他的都属于指针类型。
- LNilType and LBool 这里没看懂官方在说什么,知道的可以告知下,谢谢。
- lua 中,nil和false都是认为是错误的情况。nil表示一个无效值(在条件表达式中相当于false)。
大家有不明白的地方,推荐去看看官方怎么说的。
2.3 gopher-lua 中的调用堆栈和注册表大小
官方还介绍了性能优化这块的内容,我就不介绍了,大家感兴趣可以去看官方。
主要是对于我这种非科班出生的菜鸟来说,还是有点难度的,这里就不瞎说了,免得误导大家。哈哈......
一般来说,使用默认的方式,性能不会太差。对性能没有特别高的要求,也没有必要去折腾这个。
3、gopher-lua 中常用的API
3.1 lua 调用 Go 中的代码
test.lua 脚本内容:main.go 中的内容:- package main
- import (
- "fmt"
- lua "github.com/yuin/gopher-lua"
- )
- func main() {
- L := lua.NewState()
- defer L.Close()
- L.SetGlobal("double", L.NewFunction(Double)) /* Original lua_setglobal uses stack... */
- L.DoFile("test.lua")
- }
- func Double(L *lua.LState) int {
- fmt.Println("coming go code.............")
- lv := L.ToInt(1) /* get argument */
- L.Push(lua.LNumber(lv * 2)) /* push result */
- return 1 /* number of results */
- }
复制代码 执行结果:- coming go code.............
- 200
复制代码 上面我们已经实现了一个简单的 lua 脚本中调用 go 代码的功能。
3.2 使用Go创建模块给lua使用
上面介绍了 lua 中调用 Go中的代码,Go提供的功能不多还好,直接使用即可,但是实际项目中,既然使用到了Go和lua结合的模式,必然会存在Go提供基础功能,lua来编写业务的方式,这个时候如果还是使用上面的方式,使用起来将非常不方便。这里提供了一种方式,将Go中的功能封装成一个模块,提供给 lua 使用,这样就方便许多。
接下来我们一起看看怎么做。
mymodule.go 的内容:- package main
- import (
- "fmt"
- lua "github.com/yuin/gopher-lua"
- )
- func Loader(L *lua.LState) int {
- // register functions to the table
- mod := L.SetFuncs(L.NewTable(), exports)
- // register other stuff
- L.SetField(mod, "name", lua.LString("testName"))
- // returns the module
- L.Push(mod)
- return 1
- }
- var exports = map[string]lua.LGFunction{
- "MyAdd": MyAdd,
- }
- func MyAdd(L *lua.LState) int {
- fmt.Println("coming custom MyAdd")
- x, y := L.ToInt(1), L.ToInt(2)
- // 原谅我还不知道怎么把计算结果返回给 lua ,太菜了啦
- // 不过用上另外一个包后,我知道,具体看实战篇。
- fmt.Println(x)
- fmt.Println(y)
- return 1
- }
复制代码 main.go 的内容:- package main
- import lua "github.com/yuin/gopher-lua"
- func main() {
- L := lua.NewState()
- defer L.Close()
- L.PreloadModule("myModule", Loader)
- if err := L.DoFile("main.lua"); err != nil {
- panic(err)
- }
- }
复制代码 main.lua 的内容:- local m = require("myModule")
- m.MyAdd(10, 20)
- print(m.name)
复制代码 运行 main.go 得到执行结果:- coming custom MyAdd
- 10
- 20
- testName
复制代码 3.3 Go 调用 lua 中的代码
lua 中的代码- function TestGoCallLua(x, y)
- return x+y, x*y
- end
复制代码 go 中的代码- func TestTestGoCallLua(L *lua.LState) {
- err := L.CallByParam(lua.P{
- Fn: L.GetGlobal("TestGoCallLua"), // 获取函数引用
- NRet: 2, // 指定返回值数量,注意这里的值是 2
- Protect: true, // 如果出现异常,是panic还是返回err
- }, lua.LNumber(10), lua.LNumber(20))
- if err != nil {
- panic(err)
- }
- multiplicationRet := L.Get(-1)
- addRet := L.Get(-2)
- if str, ok := multiplicationRet.(lua.LNumber); ok {
- fmt.Println("multiplicationRet is: ", int(str))
- }
- if str, ok := addRet.(lua.LNumber); ok {
- fmt.Println("addRet is: ", int(str))
- }
- }
复制代码 具体的可以看 xxx 中的 TestTestGoCallLua 函数。
执行结果:- multiplicationRet is: 200
- addRet is: 30
复制代码 3.4 lua中使用go中定义好的类型
这里我们直接使用官方的例子:- type Person struct {
- Name string
- }
- const luaPersonTypeName = "person"
- // Registers my person type to given L.
- func registerPersonType(L *lua.LState) {
- mt := L.NewTypeMetatable(luaPersonTypeName)
- L.SetGlobal("person", mt)
- // static attributes
- L.SetField(mt, "new", L.NewFunction(newPerson))
- // methods
- L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
- }
- // Constructor
- func newPerson(L *lua.LState) int {
- person := &Person{L.CheckString(1)}
- ud := L.NewUserData()
- ud.Value = person
- L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName))
- L.Push(ud)
- return 1
- }
- // Checks whether the first lua argument is a *LUserData with *Person and returns this *Person.
- func checkPerson(L *lua.LState) *Person {
- ud := L.CheckUserData(1)
- if v, ok := ud.Value.(*Person); ok {
- return v
- }
- L.ArgError(1, "person expected")
- return nil
- }
- var personMethods = map[string]lua.LGFunction{
- "name": personGetSetName,
- }
- // Getter and setter for the Person#Name
- func personGetSetName(L *lua.LState) int {
- p := checkPerson(L)
- if L.GetTop() == 2 {
- p.Name = L.CheckString(2)
- return 0
- }
- L.Push(lua.LString(p.Name))
- return 1
- }
- func main() {
- L := lua.NewState()
- defer L.Close()
- registerPersonType(L)
- if err := L.DoString(`
- p = person.new("Steeve")
- print(p:name()) -- "Steeve"
- p:name("Alice")
- print(p:name()) -- "Alice"
- `); err != nil {
- panic(err)
- }
- }
复制代码 官方还讲解了如何使用 go 中的context 来结束lua代码的执行,这里我就不演示了,大家自行研究。
3.5 gopher_lua 中goroutine的说明
这里直接放官方的文档,大家自行理解- The LState is not goroutine-safe. It is recommended to use one LState per goroutine and communicate between goroutines by using channels.
复制代码LState不是goroutine安全的。建议每个goroutine使用一个LState,并通过使用通道在goroutine之间进行通信。
- Channels are represented by channel objects in GopherLua. And a channel table provides functions for performing channel operations.
复制代码在GopherLua中,通道由通道对象表示。通道表提供了执行通道操作的函数。这意味着,我们可以使用通道对象来创建、发送和接收消息,并使用通道表中的函数来控制通道的行为。通道是一种非常有用的并发编程工具,可以帮助我们在不同的goroutine之间进行通信和同步。通过使用GopherLua中的通道对象和通道表,我们可以轻松地在Lua代码中实现并发编程。
- Some objects can not be sent over channels due to having non-goroutine-safe objects inside itself.
复制代码某些对象无法通过通道发送,因为其内部有非goroutine安全的对象。
- a thread(state)
- a function
- an userdata
- a table with a metatable
上面这四种类型就不支持往通道中发送。
[code]package mainimport ( lua "github.com/yuin/gopher-lua" "time")func receiver(ch, quit chan lua.LValue) { L := lua.NewState() defer L.Close() L.SetGlobal("ch", lua.LChannel(ch)) L.SetGlobal("quit", lua.LChannel(quit)) if err := L.DoString(` local exit = false while not exit do -- 这个 channel 的写法是固定的 ?? channel.select( {"| |