Go的内嵌脚本语言有很多,Python语言就是一例。Python有丰富的用户群体,强大的第三方库,广泛的开源工具支持,Go的最佳伴侣应该是Python,可是Python的一些不足之处却让Go感到为难。最好用的开源的go-python库是全局单例的Python解释器,对于并发能力比较出色的Go语言来说,万恶的GIL会让Go运行时降级为单线程,很容易就成了运行的瓶颈。
看来Python这条路是走不下去了,幸好,还有Lua。
Lua作为专业的内置脚本语言,它是单线程的运行的,没有操作系统级别的多线程,同一个进程可以运行多个Lua解释器实例,数据完全独立,互不干扰。它的学习成本比Python还要低廉,普通用户大约花个30分钟就可以把Lua语言的基本特性都学完了。
Lua目前最好的golang开源项目是日本人实现的,叫GopherLua。
接下来我们逐步研究一下GopherLua如何使用,首先写一个HelloWorld
输出结果
注意我们使用NewState得到一个独立的Lua解释器实例,后续的所有操作都是基于这个实例内部进行的,全局状态限于L对象内部,没有进程级别的全局状态。如果要得到多个解释器实例,使用NewState多创建几个就行。
也许你会想到golang有如此多的goroutine,难道要每个goroutine都开一个lua解释器实例么,如果这样,内存肯定是要被撑爆的。
GopherLua考虑到了这点,它使用解释器实例池解决了这个问题。当用户想要使用Lua解释器时,从池中取出一个,用完了再还回去。因为同一个解释器可能要被多个协程使用,虽然不是同一时间被多个协程使用,要注意全局状态不要相互干扰。
下面我们使用GopherLua调用一个lua模块
斐波那契数列使用独立的lua脚本实现,golang使用DoFile加载脚本,然后使用CallByParam调用脚本中的fib全局函数,最后获取返回结果打印输出。
GopherLua的函数调用是通过堆栈来进行的,调用前将参数压栈,完事后将结果放入堆栈中,调用方在堆栈顶部拿结果。
接下来我们将lua面向对象的例子翻译成对应的GopherLua代码。也就是使用GopherLua提供的API一步一步组装成复杂的lua对象定义及其实现。
上面是一个简单的Counter对象,提供incr和get两个操作进行自增和获取当前值。如果你不了解lua的面向对象特性,请搜索一下Lua菜鸟教程进行阅读
我们来把上面的lua代码翻译成一个等价的GopherLua代码
换成了Go代码就比上面的lua代码复杂太多了,看起来也远不及lua直接。特别是返回值不是返回值,而是返回值的个数,返回值要往栈里压。还有参数也不是直接拿到的,而要从栈里面挨个拿。函数调用在形式上像极了汇编语言。
GopherLua除了可以满足基本的lua需要,还将Go语言特有的高级设计直接移植到lua环境中,使得内嵌的脚本也具备了一些高级的特性
可以使用context.WithTimeout对执行的lua脚本进行超时控制
可以使用context.WithCancel打断正在执行的lua脚本
多个lua解释器实例之间还可以通过channel共享数据
支持多路复用选择器select
使用Lua作为内嵌脚本的另外一个重要优势在于Lua非常轻量级,占用内存极小。接下来我们使用下面的脚本来测试测试单个Lua解释器实例占用的内存大小。
上面的代码开启了10000个lua解释器实例,每个解释器实例调用一次斐波拉契函数输出结果。然后在退出之前休眠100s便于我们使用top命令观察进程的内存占用。
观察发现在笔者的mac电脑上,整个进程占据了大约1.7G左右的内存。平摊下来大约每个解释器实例占据170k左右的内存空间,相比Python动辄几个M大小的空间来说,这已经非常节约了,但实际上lua在节约内存的道路上可以走的更远。GopherLua提供了对Lua运行时进行裁剪的功能,这能使得它占用的内存更小。
当内嵌脚本要被终端用户使用时,需要考虑一些安全问题。比如用户编写的脚本代码使用了lua提供的库函数访问了不该访问的文件,或者调用了一些不该调用的系统模块。这些不良行为都会给系统带来威胁,需要进行约束。
GopherLua可以创建一个非常干净的Lua解释器实例,不加载任何系统模块。然后由程序员自己提供的模块注册进去,给内嵌脚本提供一个安全的沙箱运行环境。
阅读相关文章,关注微信公众号/知乎专栏/头条号【码洞】