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 mainimport 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.luafunction fib(n) if n < 2 then return 1 end return fib(n-1) + fib(n-2)end// main.gopackage mainimport ( "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 cendfunction Counter:incr(v) self.value = self.value + v return self.valueendfunction Counter:get() return self.valueendcounter = Counter:new(100)for i=1,10 do print(counter:incr(i))endprint(counter:get())

上面是一個簡單的Counter對象,提供incr和get兩個操作進行自增和獲取當前值。如果你不了解lua的面向對象特性,請閱讀一下菜鳥教程

Lua 面向對象 | 菜鳥教程www.runoob.com

我們來把上面的lua代碼翻譯成一個等價的GopherLua代碼

package mainimport ( 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 mainimport ( "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解釋器實例,不載入任何系統模塊。然後由程序員自己提供的模塊註冊進去,給內嵌腳本提供一個安全的沙箱運行環境。

閱讀相關文章,請關注知乎專欄【碼洞】


推薦閱讀:

有哪些公司在用 Google 的 Go 語言?成熟度和 Erlang 比起來如何?
如何看待許式偉談Go Erlang並發編程差異?
為什麼 Go 語言如此不受待見?
Go語言在Linux中後台運行的問題?
Go語言做Web應用開發的框架,哪一個更適合入門?

TAG:Go语言 | Lua | 高并发 |