揭秘!用標准Go語言能寫腳本嗎?


揭秘!用標准Go語言能寫腳本嗎? https://mp.weixin.qq.com/s/NTA-Mf14gj-6nDTDekGNVA

揭秘!用標准Go語言能寫腳本嗎?

圖片

 

導語 | Go作為一種編譯型語言,經常用於實現后台服務的開發。由於Go初始的開發大佬都是C的老牌使用者,因此Go中保留了不少C的編程習慣和思想,這對C/C++ 和PHP開發者來說非常有吸引力。作為編譯型語言的特性,也讓Go在多協程環境下的性能有不俗的表現。但腳本語言則幾乎都是解釋型語言,那么Go怎么就和腳本扯上關系了?請讀者帶着這個疑問,“聽” 本文給你娓娓道來~

 

一、什么樣的語言可以作為腳本語言?

 

程序員們都知道,高級程序語言從運行原理的角度來說可以分成兩種:編譯型語言、解釋型語言。Go就是一個典型的編譯型語言。

 

  • 編譯型語言就是需要使用編譯器,在程序運行之前將代碼編譯成操作系統能夠直接識別的機器碼文件。運行時,操作系統直接拉起該文件,在CPU中直接運行。

 

  • 解釋型語言則是在代碼運行之前,需要先拉起一個解釋程序,使用這個程序在運行時就可以根據代碼的邏輯執行。

 

編譯型語言的典型例子就是匯編語言、C、C++、Objective-C、Go、Rust等等。

 

解釋型語言的典型例子就是JavaScript、PHP、Shell、Python、Lua等等。

 

至於Java,從JVM的角度,它是一個編譯型語言,因為編譯出來的二進制碼可以直接在JVM上執行。但從CPU的角度,它依然是一個解釋型語言,因為CPU並不直接運行代碼,而是間接地通過JVM解釋Java二進制碼從而實現邏輯運行。

 

所謂的 “腳本語言” 則是另外的一個概念,這一般指的是設計初衷就是用來開發一段小程序或者是小邏輯,然后使用預設的解釋器解釋這段代碼並執行的程序語言。這是一個程序語言功能上的定義,理論上所有解釋型語言都可以很方便的作為腳本語言,但是實際上我們並不會這么做,比如說PHP和JS就很少作為腳本語言使用。

 

可以看到,解釋型語言天生適合作為腳本語言,因為它們原本就需要使用運行時來解釋和運行代碼。將運行時稍作改造或封裝,就可以實現一個動態拉起腳本的功能。

 

但是,程序員們並不信邪,ta們從來就沒有放棄把編譯型語言變成腳本語言的努力。

 

 

二、為什么需要用GO寫腳本?

 

首先回答一個問題:為什么我們需要嵌入腳本語言?答案很簡單,編譯好的程序邏輯已經固定下來了,這個時候,我們需要添加一個能力,能夠在運行時調整某些部分的功能邏輯,實現這些功能的靈活配置。

 

在這方面,其實項目組分別針對Go和Lua都有了比較成熟的應用,使用的分別是yaegi(https://github.com/traefik/yaegi)和gopher(https://github.com/yuin/gopher-lua)。關於后者的文章已經很多,本文便不再贅述。這里我們先簡單列一下使用yaegi的優勢:

 

  • 完全遵從官方Go語法(1.16 和 1.17),因此無需學習新的語言。不過泛型暫不支持。

 

  • 可調用Go原生庫,並且可擴展第三方庫,進一步簡化邏輯。

 

  • 與主調方的Go程序可以直接使用struct進行參數傳遞,大大簡化開發。

 

可以看到,yaegi的三個優勢中,都有“簡”字。便於上手、便於對接,就是它最大的優勢。

 

 

三、快速上手

 

 

這里,我們寫一段最簡單的代碼,代碼的功能是斐波那契數:

 

package plugin
func Fib(n int) int { return fib(n, 0, 1)}
func fib(n, a, b int) int { if n == 0 { return a } else if n == 1 { return b } return fib(n-1, b, a+b)}

 

令上方的代碼成為一個string常量:const src=...,然后使用yaegi封裝並在代碼中調用:

 

package main 
import ( "fmt"
"github.com/traefik/yaegi/interp" "github.com/traefik/yaegi/stdlib")
func main() { intp := interp.New(interp.Options{}) // 初始化一個 yaegi 解釋器 intp.Use(stdlib.Symbols) // 允許腳本調用(幾乎)所有的 Go 官方 package 代碼
intp.Eval(src) // src 就是上面的 Go 代碼字符串 v, _ := intp.Eval("plugin.Fib") fu := v.Interface().(func(int) int)
fmt.Println("Fib(35) =", fu(35))}
// Output:// Fib(35) = 9227465
const src = `package plugin
func Fib(n int) int { return fib(n, 0, 1)}
func fib(n, a, b int) int { if n == 0 { return a } else if n == 1 { return b } return fib(n-1, b, a+b)}`

 

我們可以留意到fu變量,這直接就是一個函數變量。換句話說,yaegi直接將腳本中定義的函數,解釋后向主調方程序直接暴露成同一結構的函數,調用方可以直接像調用普通函數一樣調用它,而不是像其他腳本庫一樣,需要調用一個專門的傳參函數、再獲得返回值、最后再將返回值進行轉換。

 

從這一點來說就顯得非常非常的友好,這意味着運行時,和腳本之間可以直接傳遞參數,而不需要中間轉換。

 

 

四、自定義數據結構傳遞

 

前文說到,yaegi的一個極大的優勢,是可以直接傳遞自定義struct格式。

 

這里,我先拋出如何傳遞自定義數據結構的方法,然后再更進一步講yaegi對第三方庫的支持。

 

比如說,我定義了一個自定義的數據結構(https://github.com/Andrew-M-C/go.util/blob/master/slice/lcs.go#L91),並且希望在Go腳本中進行傳遞:

 

package slice
// github.com/Andrew-M-C/go.util/slice
// ...
type Route struct { XIndexes []int YIndexes []int}

 

那么,在對yaegi解釋器進行初始化的時候,我們可以在intp變量初始化完成之后,調用以下代碼進行符號表的初始化:

 

 intp := interp.New(interp.Options{})
intp.Use(stdlib.Symbols) intp.Use(map[string]map[string]reflect.Value{ "github.com/Andrew-M-C/go.util/slice/slice": { "Route": reflect.ValueOf((*slice.Route)(nil)), }, })

 

這樣,腳本在調用的時候,除了原生庫之外,也可以使用 github.com/Andrew-M-C/go.util/slice中的Route結構體。這就實現了struct的原生傳遞。

 

這里需要注意的是:Use函數傳入的map,其key並不是package的名稱,而是package路徑+package名稱的組合。比如說引入一個package,路徑:github.com/A/B,那么它的package路徑就是 “github.com/A/B”,package名稱是B,連在一起的key就是:github.com/A/B/B,注意后面被重復了兩次的“B”——筆者就被這坑過,卡了好幾天。

 

 

五、Yaegi支持第三方庫

 

(一)原理

 

我們可以留意一下上文的例子中intp.Use(stdlib.Symbols) 這一句,這可以說是yaegi區別於其他Go腳本庫的實現之一。這一句的含義是:使用標准庫的符號表。

 

Yaegi解釋器分析了Go腳本的語法之后,會將其中的符號調用與符號表中的目標進行鏈接。而stdlib.Symbols就導出了Go中幾乎所有的標准庫的符號。不過從安全角度,yaegi禁止了諸如poweroff、reboot等的高權限系統調用。

 

因此,我們自然而然地就可以想到,我們也可以把自定義的符號表定義進去——這也就是Use函數的作用,將各符號的原型定義給yaegi就能夠實現第三方庫的支持了。

 

當然,這種方法只能對腳本所能引用的第三方庫進行預先定義,而不支持在腳本中動態加載未定義的第三方庫。即便如此,這也極大地擴展了yaegi腳本的功能。

 

 

(二)符號解析

 

前文中,我們手動在代碼中指定了需要引入的第三方符號表。但是對於很長的代碼,一個符號一個符號地敲,實在是太麻煩了。其實yaegi提供了一個工具,能夠分析目標package並輸出符號列表。我們可以看看yaegi的stdlib庫作為例子,它就是對Go原生的package文件進行了解釋,並找到符號表,所使用的package就是yaegi附帶開發的一個工具。

 

因此,我們就可以借用這個功能,結合go generate,在代碼中動態地生成符號表配置代碼。

 

還是以上面的github.com/Andrew-M-C/go.util/slice為例子,在引用yaegi的位置,添加以下go generate:

 

//go:generate go install github.com/traefik/yaegi/cmd/yaegi@v0.10.0//go:generate yaegi extract github.com/Andrew-M-C/go.util/slice

 

工具會在當前目錄下,生成一個github_com-Andrew-M-C-go_util-slice.go文件,文件的內容就是符號表配置。這樣一來,我們就不用費時間去一個一個導出符號啦。

 

 

六、與其他腳本方案的對比

 

(一)功能對比

 

我們在調研了yaegi之外,也另外調研和對比了tengo和使用Lua的 gopher-lua。其中后者也是團隊應用得比較成熟的庫。

 

筆者需要特別強調的是:tengo的標題雖然說自己用的是Go,但實際上是掛羊頭賣狗肉。tengo使用是自己的一套獨立語法,與官方Go完全不兼容,甚至乎連相似都稱不上。我們應當把它當作另一種腳本語言來看。

 

這三種方案的對比如下:

 

圖片

 

總而言之:

 

  • gopher的優勢在於性能。

 

  • yaegi的優勢在於Go原生語法,以及可以接受的性能。

 

  • tengo的優勢?對於筆者的這一使用場景來說,不存在的。

 

但是yaegi也有很明顯的不足:

 

  • 它依然處於0.y.z版本的階段,也就是說這只是beta版本,后續的API可能會有比較大的變化。

 

  • Go官方語法的大方向是支持泛型,而yaegi目前是不支持泛型的。后續需要關注yaegi在這方便的迭代情況。

 

 

(二)性能對比

 

下文的表格比較多,這里先拋這三個庫的對比結論吧:

 

  • 從純算力性能上看,gopher擁有壓倒性的優勢。

 

  • yaegi的性能很穩定,大約是gopher的1/5~1/4之間。

 

  • 非計算密集型的場景下,tengo的性能比較糟糕。平均場景也是最差的。

 

  • 簡單的a+b

 

這是一個簡單的邏輯封裝,就是普通的res:=a+b,這是一個極限情況的測試。測試結果如下:

 

圖片

 

結果讓人大跌眼鏡,對於特別簡單的腳本,tengo的耗時極高,很可能是在進入和退出tengo VM時,消耗了過多的資源。

 

而gopher則表現出了優異的性能。讓人印象非常深刻。

 

  • 條件判斷

 

該邏輯也很簡單,判斷輸入數是否大於零。測試結果與簡單加法類似,如下:

 

圖片

 

  • 斐波那契數

 

前面兩個性能測試過於極限,只能作參考用。在tengo的README中,聲稱其擁有非常高的性能,可與gopher和原生Go相比,並且還能壓倒yaegi。既然tengo這么有信心,並且還給出了其使用的Fib函數,那么我就來測一下。測試結果如下:

 

圖片

 

 

七、工程應用注意要點

 

 

在實際工程應用中,針對yaegi,筆者鎖定這樣的一個應用場景:使用Go運行時程序,調用Go腳本。我需要限制這個腳本完成有限的功能(比如數據檢查、過濾、清洗)。因此,我們應該限制腳本可調用的能力。我們可以通過刪除stdlib.Symbols表中的部分package來實現,筆者在實際應用中,刪除了以下的package符號:

 

  • os/xxx

  • net/xxx

  • log

  • io/xxx

  • database/xxx

  • runtime

 

此外,雖然yaegi直接將腳本函數暴露出來可以直接調用,但是主程序不能對腳本的可靠性做任何的假設。換句話說,腳本可能會panic,或者是修改了主程序的變量,從而導致主程序panic。為了避免這一點,我們要將腳本放在一個受限的環境里運行,除了前面通過限制yaegi可調用的package的方式之外,還應該限制調用腳本的方式。包括但不限於以下幾個手段:

 

  • 將調用邏輯放在獨立的goroutine中調用,並且通過recover函數捕獲異常。

 

  • 不直接將主程序的變量等內存信息暴露給腳本,傳參時候,需要考慮將參數復制后再傳遞,或者是腳本非法返回的可能性。

 

  • 如無必要,可以禁止腳本開啟新的goroutine。由於go是一個關鍵字,因此全文匹配一下正則“\sgo”就行(注意空格字符)。

 

  • 腳本的運行時間也需要進行限制,或者是監控。如果腳本有bug出現了無限循環,那么主調方應能夠脫離這個腳本函數,回到主流程中。

 

當然,文中充滿了對tengo的不推崇,也只是在筆者的這種使用場景下,tengo沒有任何優勢而已,請讀者辯證閱讀,也歡迎補充和指正~

 

( 轉載須取得作者同意,未經許可,禁止二次轉載 )

 

 

 作者簡介

 

張敏

騰訊高級后台工程師

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM