首发于码洞
GopherLua基础入门

GopherLua基础入门

Go的内嵌脚本语言有很多,Python语言就是一例。Python有丰富的用户群体,强大的第三方库,广泛的开源工具支持,Go的最佳伴侣应该是Python,可是Python的一些不足之处却让Go感到为难。最好用的开源的go-python库是全局单例的Python解释器,对于并发能力比较出色的Go语言来说,万恶的GIL会让Go运行时降级为单线程,很容易就成了运行的瓶颈。

看来Python这条路是走不下去了,幸好,还有Lua。

Lua作为专业的内置脚本语言,它是单线程的运行的,没有操作系统级别的多线程,同一个进程可以运行多个Lua解释器实例,数据完全独立,互不干扰。它的学习成本比Python还要低廉,普通用户大约花个30分钟就可以把Lua语言的基本特性都学完了。

Lua目前最好的golang开源项目是日本人实现的,叫GopherLua。

yuin/gopher-luagithub.com图标

接下来我们逐步研究一下GopherLua如何使用,首先写一个HelloWorld

package main

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

func main() {
	L := lua.NewState() // 创建一个lua解释器实例
	defer L.Close()
        // 执行字符串语句
	if err := L.DoString(`print("hello")`); err != nil {
		panic(err)
	}
}

输出结果

注意我们使用NewState得到一个独立的Lua解释器实例,后续的所有操作都是基于这个实例内部进行的,全局状态限于L对象内部,没有进程级别的全局状态。如果要得到多个解释器实例,使用NewState多创建几个就行。

也许你会想到golang有如此多的goroutine,难道要每个goroutine都开一个lua解释器实例么,如果这样,内存肯定是要被撑爆的。

GopherLua考虑到了这点,它使用解释器实例池解决了这个问题。当用户想要使用Lua解释器时,从池中取出一个,用完了再还回去。因为同一个解释器可能要被多个协程使用,虽然不是同一时间被多个协程使用,要注意全局状态不要相互干扰。

下面我们使用GopherLua调用一个lua模块

// fib.lua
function fib(n)
	if n < 2 then
		return 1
	end
	return fib(n-1) + fib(n-2)
end

// main.go
package main

import (
	"fmt"

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

func main() {
	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=10
	if err != nil {
		panic(err)
	}
	// 获取返回结果
	ret := L.Get(-1)
	// 从堆栈中扔掉返回结果
	L.Pop()
	// 打印结果
	res, ok := ret.(lua.LNumber)
	if ok {
		fmt.Println(int(res))
	} else {
		fmt.Println("unexpected result")
	}
}

斐波那契数列使用独立的lua脚本实现,golang使用DoFile加载脚本,然后使用CallByParam调用脚本中的fib全局函数,最后获取返回结果打印输出。

GopherLua的函数调用是通过堆栈来进行的,调用前将参数压栈,完事后将结果放入堆栈中,调用方在堆栈顶部拿结果。

接下来我们将lua面向对象的例子翻译成对应的GopherLua代码。也就是使用GopherLua提供的API一步一步组装成复杂的lua对象定义及其实现。

Counter = {}

function Counter:new(v)
	c = {value=v or 0}
	setmetatable(c, self)
	self.__index = self
	return c
end

function Counter:incr(v)
	self.value = self.value + v
	return self.value
end

function Counter:get()
	return self.value
end

counter = Counter:new(100)
for i=1,10 do
	print(counter:incr(i))
end
print(counter:get())

上面是一个简单的Counter对象,提供incr和get两个操作进行自增和获取当前值。如果你不了解lua的面向对象特性,请阅读一下菜鸟教程

Lua 面向对象 | 菜鸟教程www.runoob.com

我们来把上面的lua代码翻译成一个等价的GopherLua代码

package main

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

func main() {
	L := lua.NewState()
	defer L.Close()
	meta := L.NewTable()
	L.SetGlobal("Counter", meta)
	// 注册函数
	L.SetField(meta, "new", L.NewFunction(newCounter))
	L.SetField(meta, "incr", L.NewFunction(incrCounter))
	L.SetField(meta, "get", L.NewFunction(getCounter))
	// 使用lua代码测试效果
	err := L.DoString(`
		counter = Counter:new(100)
		for i=1,10 do
			print(counter:incr(i))
		end
		print(counter:get())
	`)
	if err != nil {
		panic(err)
	}
}

func newCounter(L *lua.LState) int {
	c := L.NewTable()
	self := L.CheckTable(1)
	value := lua.LNumber(0)
	// 第二个为可选参数
	if L.GetTop() >= 2 {
		value = L.CheckNumber(2)
	}
	L.SetField(c, "value", value)
	L.SetMetatable(c, self)
	L.SetField(self, "__index", self)
	// 返回值压栈
	L.Push(c)
	// 返回[函数返回值的个数]
	return 1
}

func incrCounter(L *lua.LState) int {
	self := L.CheckTable(1)
	value := lua.LNumber(0)
	// 第二个为可选参数
	if L.GetTop() >= 2 {
		value = L.CheckNumber(2)
	}
	current := L.GetField(self, "value").(lua.LNumber)
	current += value
	L.SetField(self, "value", current)
	// 返回值压栈
	L.Push(current)
	// 返回[函数返回值的个数]
	return 1
}

func getCounter(L *lua.LState) int {
	self := L.CheckTable(1)
	value := L.GetField(self, "value").(lua.LNumber)
	// 返回值压栈
	L.Push(value)
	// 返回[函数返回值的个数]
	return 1
}

换成了Go代码就比上面的lua代码复杂太多了,看起来也远不及lua直接。特别是返回值不是返回值,而是返回值的个数,返回值要往栈里压。还有参数也不是直接拿到的,而要从栈里面挨个拿。函数调用在形式上像极了汇编语言。

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

  1. 可以使用context.WithTimeout对执行的lua脚本进行超时控制
  2. 可以使用context.WithCancel打断正在执行的lua脚本
  3. 多个lua解释器实例之间还可以通过channel共享数据
  4. 支持多路复用选择器select

使用Lua作为内嵌脚本的另外一个重要优势在于Lua非常轻量级,占用内存极小。接下来我们使用下面的脚本来测试单个Lua解释器实例占用的内存大小。

package main

import (
	"time"

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

func main() {
	for i := 0; i < 10000; i++ {
		L := lua.NewState()
		defer L.Close()
		L.DoString(`
			function fib(n)
				if n < 2 then
					return 1
				end
				return fib(n-1) + fib(n-2)
			end
			print(fib(10))
		`)
	}
	time.Sleep(100 * time.Second)
}

上面的代码开启了10000个lua解释器实例,每个解释器实例调用一次斐波拉契函数输出结果。然后在退出之前休眠100s便于我们使用top命令观察进程的内存占用。

观察发现在笔者的mac电脑上,整个进程占据了大约1.7G左右的内存。平摊下来大约每个解释器实例占据170k左右的内存空间,相比Python动辄几个M大小的空间来说,这已经非常节约了,但实际上lua在节约内存的道路上可以走的更远。GopherLua提供了对Lua运行时进行裁剪的功能,这能使得它占用的内存更小。

当内嵌脚本要被终端用户使用时,需要考虑一些安全问题。比如用户编写的脚本代码使用了lua提供的库函数访问了不该访问的文件,或者调用了一些不该调用的系统模块。这些不良行为都会给系统带来威胁,需要进行约束。

GopherLua可以创建一个非常干净的Lua解释器实例,不加载任何系统模块。然后由程序员自己提供的模块注册进去,给内嵌脚本提供一个安全的沙箱运行环境。

阅读相关文章,请关注知乎专栏【码洞】

编辑于 2018-02-01

文章被以下专栏收录