本文由go語言教學專欄為大家介紹Golang和Lua ,希望對需要的朋友有幫助!
在 GitHub 玩耍時,偶然發現了 gopher-lua ,這是一台純 Golang 實作的 Lua 虛擬機器。我們知道 Golang 是靜態語言,而 Lua 是動態語言,Golang 的效能和效率各語言中表現得非常不錯,但在動態能力上,肯定是無法與 Lua 相比。那麼如果我們能夠將二者結合起來,就能綜合二者各自的長處了(手動滑稽。
在項目Wiki 中,我們可以知道gopher-lua 的執行效率和性能僅比C 實現的bindings 差。因此從性能方面考慮,這應該是一款非常不錯的虛擬機方案。
#Hello World
這裡給出了一個簡單的Hello World程序。我們先是新建了一個虛擬機,隨後對其進行了DoString(...) 解釋執行lua 代碼的操作,最後將虛擬機關閉。執行程序,我們將在命令行看到"Hello World" 的字符字串。
package main import ( "github.com/yuin/gopher-lua" ) func main() { l := lua.NewState() defer l.Close() if err := l.DoString(`print("Hello World")`); err != nil { panic(err) } } // Hello World
提前編譯
在查看上述DoString(...) 方法的呼叫鏈後,我們發現每執行一次DoString(...) 或DoFile(...) ,都會各執行一次parse 和compile 。
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 程式碼將被執行多次(如在http server 中,每次請求將執行相同Lua 程式碼)的場景下,如果我們能夠對程式碼進行提前編譯,那麼應該能夠減少parse 和compile 的開銷(如果這屬於hotpath 程式碼)。根據Benchmark 結果,提前編譯確實能夠減少不必要的開銷。
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 程式碼被執行的場景下,除了可使用提前編譯最佳化效能外,我們還可以引入虛擬機器實例池。
因為新建一個Lua 虛擬機會涉及到大量的記憶體分配操作,如果採用每次運行都重新創建和銷毀的方式的話,將消耗大量的資源。引入虛擬機實例池,能夠復用虛擬機,減少不必要的開銷。
func BenchmarkRunWithoutPool(b *testing.B) { for i := 0; i < b.N; i++ { l := lua.NewState() _ = l.DoString(`a = 1 + 1`) l.Close() } } func BenchmarkRunWithPool(b *testing.B) { pool := newVMPool(nil, 100) for i := 0; i < b.N; i++ { l := pool.get() _ = l.DoString(`a = 1 + 1`) pool.put(l) } } // goos: darwin // goarch: amd64 // pkg: glua // BenchmarkRunWithoutPool-8 10000 129557 ns/op 262599 B/op 826 allocs/op // BenchmarkRunWithPool-8 100000 19320 ns/op 85626 B/op 67 allocs/op // PASS // ok glua 3.467s
Benchmark 結果顯示,虛擬機實例池的確能夠減少很多內存分配操作。
下面給出了README 提供的實例池實現,但注意到實作在初始狀態時,並未建立足夠多的虛擬機器實例(初始時,實例數為0),以及存在slice 的動態擴容問題,這都是值得改進的地方。
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), }
模組呼叫
gopher-lua 支援Lua 呼叫Go 模組,個人覺得,這是一個非常令人振奮的功能點,因為在Golang 程式開發中,我們可能設計出許多常用的模組,這種跨語言呼叫的機制,使得我們能夠對程式碼、工具進行重複使用。
當然,除此之外,也存在 Go 呼叫 Lua 模組,但個人感覺後者是沒啥必要的,所以在這裡並沒有涉及後者的內容。
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
變數污染
當我們使用實例池減少開銷時,會引入另一個棘手的問題:由於同一個虛擬機器可能會被多次執行同樣的Lua 程式碼,進而變動了其中的全域變數。如果程式碼邏輯依賴全域變量,那麼可能會出現難以預測的運行結果(這有點資料庫隔離性中的「不可重複讀取」的味道)。
全域變數
如果我們需要限制 Lua 程式碼只能使用局部變量,那麼站在這個出發點上,我們需要對全域變數做出限制。那問題來了,該如何實現呢?
我們知道,Lua 是編譯成字節碼,再被解釋執行的。那麼,我們可以在編譯字節碼的階段中,對全域變數的使用作出限制。在查閱 Lua 虛擬機器指令後,發現涉及全域變數的指令有兩個:GETGLOBAL(Opcode 5)和 SETGLOBAL(Opcode 7)。
到這裡,已經有了大致的想法:我們可透過判斷字節碼是否含有 GETGLOBAL 和 SETGLOBAL 進而限製程式碼的全域變數的使用。至於字節碼的獲取,可透過呼叫 CompileString(...) 和 CompileFile(...) ,得到 Lua 程式碼的 FunctionProto ,而其中的 Code 屬性即為字節碼 slice,類型為 []uint32 。
在虛擬機器實作程式碼中,我們可以找到一個根據字節碼輸出對應 OpCode 的工具函數。
// 获取对应指令的 OpCode func opGetOpCode(inst uint32) int { return int(inst >> 26) }
有了這個工具函數,我們即可實作對全域變數的檢查。
package main // ... func CheckGlobal(proto *lua.FunctionProto) error { for _, code := range proto.Code { switch opGetOpCode(code) { case lua.OP_GETGLOBAL: return errors.New("not allow to access global") case lua.OP_SETGLOBAL: return errors.New("not allow to set global") } } // 对嵌套函数进行全局变量的检查 for _, nestedProto := range proto.FunctionPrototypes { if err := CheckGlobal(nestedProto); err != nil { return err } } return nil } func TestCheckGetGlobal(t *testing.T) { l := lua.NewState() proto, _ := CompileString(`print(_G)`) if err := CheckGlobal(proto); err == nil { t.Fail() } l.Close() } func TestCheckSetGlobal(t *testing.T) { l := lua.NewState() proto, _ := CompileString(`_G = {}`) if err := CheckGlobal(proto); err == nil { t.Fail() } l.Close() }
模組
除變數可能被污染外,導入的 Go 模組也有可能在運行期間被竄改。因此,我們需要一種機制,確保導入到虛擬機器的模組不會被竄改,即導入的物件是唯讀的。
查閱相關部落格後,我們可以對 Table 的 __newindex 方法的修改,將模組設定為唯讀模式。
package main import ( "fmt" "github.com/yuin/gopher-lua" ) // 设置表为只读 func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData { ud := l.NewUserData() mt := l.NewTable() // 设置表中域的指向为 table l.SetField(mt, "__index", table) // 限制对表的更新操作 l.SetField(mt, "__newindex", l.NewFunction(func(state *lua.LState) int { state.RaiseError("not allow to modify table") return 0 })) ud.Metatable = mt return ud } func load(l *lua.LState) int { mod := l.SetFuncs(l.NewTable(), exports) l.SetField(mod, "name", lua.LString("gomodule")) // 设置只读 l.Push(SetReadOnly(l, mod)) return 1 } var exports = map[string]lua.LGFunction{ "goFunc": goFunc, } func goFunc(l *lua.LState) int { fmt.Println("golang") return 0 } func main() { l := lua.NewState() l.PreloadModule("gomodule", load) // 尝试修改导入的模块 if err := l.DoString(`local m = require("gomodule");m.name = "hello world"`); err != nil { fmt.Println(err) } l.Close() } // <string>:1: not allow to modify table
寫在最後
Golang 和Lua 的融合,開闊了我的視野:原來靜態語言和動態語言還能這麼融合,靜態語言的運作高效率,配合動態語言的開發高效率,想想都興奮(逃。
在網路上找了很久,發現並沒有關於Go-Lua 的技術分享,只找到了一篇稍微有點聯繫的文章(京東三級列表頁持續架構優化— Golang Lua (OpenResty) 最佳實踐),而在這篇文章中, Lua 還是跑在C 上的。由於資訊的缺乏以及本人(學生黨)開發經驗不足的原因,並不能很好地評估該方案在實際生產中的可行性。因此,本篇文章也只能當作「閒文」了,哈哈。
以上是Golang和Lua相遇會擦出什麼火花?的詳細內容。更多資訊請關注PHP中文網其他相關文章!