<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          揭秘!用標(biāo)準(zhǔn)Go語(yǔ)言能寫(xiě)腳本嗎?

          共 5838字,需瀏覽 12分鐘

           ·

          2022-07-04 17:45

          導(dǎo)語(yǔ) | Go作為一種編譯型語(yǔ)言,經(jīng)常用于實(shí)現(xiàn)后臺(tái)服務(wù)的開(kāi)發(fā)。由于Go初始的開(kāi)發(fā)大佬都是C的老牌使用者,因此Go中保留了不少C的編程習(xí)慣和思想,這對(duì)C/C++ 和PHP開(kāi)發(fā)者來(lái)說(shuō)非常有吸引力。作為編譯型語(yǔ)言的特性,也讓Go在多協(xié)程環(huán)境下的性能有不俗的表現(xiàn)。但腳本語(yǔ)言則幾乎都是解釋型語(yǔ)言,那么Go么就和腳本扯上關(guān)系了?請(qǐng)讀者帶著這個(gè)疑問(wèn),“聽(tīng)” 本文給你娓娓道來(lái)~


          一、什么樣的語(yǔ)言可以作為腳本語(yǔ)言?


          程序員們都知道,高級(jí)程序語(yǔ)言從運(yùn)行原理的角度來(lái)說(shuō)可以分成兩種:編譯型語(yǔ)言、解釋型語(yǔ)言。Go就是一個(gè)典型的編譯型語(yǔ)言。


          • 編譯型語(yǔ)言就是需要使用編譯器,在程序運(yùn)行之前將代碼編譯成操作系統(tǒng)能夠直接識(shí)別的機(jī)器碼文件。運(yùn)行時(shí),操作系統(tǒng)直接拉起該文件,在CPU中直接運(yùn)行。


          • 解釋型語(yǔ)言則是在代碼運(yùn)行之前,需要先拉起一個(gè)解釋程序,使用這個(gè)程序在運(yùn)行時(shí)就可以根據(jù)代碼的邏輯執(zhí)行。


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


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


          至于Java,從JVM的角度,它是一個(gè)編譯型語(yǔ)言,因?yàn)榫幾g出來(lái)的二進(jìn)制碼可以直接在JVM上執(zhí)行。但從CPU的角度,它依然是一個(gè)解釋型語(yǔ)言,因?yàn)镃PU并不直接運(yùn)行代碼,而是間接地通過(guò)JVM解釋Java二進(jìn)制碼從而實(shí)現(xiàn)邏輯運(yùn)行。


          所謂的 “腳本語(yǔ)言” 則是另外的一個(gè)概念,這一般指的是設(shè)計(jì)初衷就是用來(lái)開(kāi)發(fā)一段小程序或者是小邏輯,然后使用預(yù)設(shè)的解釋器解釋這段代碼并執(zhí)行的程序語(yǔ)言。這是一個(gè)程序語(yǔ)言功能上的定義,理論上所有解釋型語(yǔ)言都可以很方便的作為腳本語(yǔ)言,但是實(shí)際上我們并不會(huì)這么做,比如說(shuō)PHP和JS就很少作為腳本語(yǔ)言使用。


          可以看到,解釋型語(yǔ)言天生適合作為腳本語(yǔ)言,因?yàn)樗鼈冊(cè)揪托枰褂眠\(yùn)行時(shí)來(lái)解釋和運(yùn)行代碼。將運(yùn)行時(shí)稍作改造或封裝,就可以實(shí)現(xiàn)一個(gè)動(dòng)態(tài)拉起腳本的功能。


          但是,程序員們并不信邪,ta們從來(lái)就沒(méi)有放棄把編譯型語(yǔ)言變成腳本語(yǔ)言的努力。



          二、為什么需要用GO寫(xiě)腳本?


          首先回答一個(gè)問(wèn)題:為什么我們需要嵌入腳本語(yǔ)言?答案很簡(jiǎn)單,編譯好的程序邏輯已經(jīng)固定下來(lái)了,這個(gè)時(shí)候,我們需要添加一個(gè)能力,能夠在運(yùn)行時(shí)調(diào)整某些部分的功能邏輯,實(shí)現(xiàn)這些功能的靈活配置。


          在這方面,其實(shí)項(xiàng)目組分別針對(duì)Go和Lua都有了比較成熟的應(yīng)用,使用的分別是yaegi(https://github.com/traefik/yaegi)gopher(https://github.com/yuin/gopher-lua)。關(guān)于后者的文章已經(jīng)很多,本文便不再贅述。這里我們先簡(jiǎn)單列一下使用yaegi的優(yōu)勢(shì):


          • 完全遵從官方Go語(yǔ)法(1.16 和 1.17),因此無(wú)需學(xué)習(xí)新的語(yǔ)言。不過(guò)泛型暫不支持。


          • 可調(diào)用Go原生庫(kù),并且可擴(kuò)展第三方庫(kù),進(jìn)一步簡(jiǎn)化邏輯。


          • 與主調(diào)方的Go程序可以直接使用struct進(jìn)行參數(shù)傳遞,大大簡(jiǎn)化開(kāi)發(fā)。


          可以看到,yaegi的三個(gè)優(yōu)勢(shì)中,都有“簡(jiǎn)”字。便于上手、便于對(duì)接,就是它最大的優(yōu)勢(shì)。



          三、快速上手


          這里,我們寫(xiě)一段最簡(jiǎn)單的代碼,代碼的功能是斐波那契數(shù):


          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)}


          令上方的代碼成為一個(gè)string常量:const src=...,然后使用yaegi封裝并在代碼中調(diào)用:


          package main 
          import ( "fmt"
          "github.com/traefik/yaegi/interp" "github.com/traefik/yaegi/stdlib")
          func main() { intp := interp.New(interp.Options{}) // 初始化一個(gè) yaegi 解釋器 intp.Use(stdlib.Symbols) // 允許腳本調(diào)用(幾乎)所有的 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變量,這直接就是一個(gè)函數(shù)變量。換句話說(shuō),yaegi直接將腳本中定義的函數(shù),解釋后向主調(diào)方程序直接暴露成同一結(jié)構(gòu)的函數(shù),調(diào)用方可以直接像調(diào)用普通函數(shù)一樣調(diào)用它,而不是像其他腳本庫(kù)一樣,需要調(diào)用一個(gè)專門(mén)的傳參函數(shù)、再獲得返回值、最后再將返回值進(jìn)行轉(zhuǎn)換。


          從這一點(diǎn)來(lái)說(shuō)就顯得非常非常的友好,這意味著運(yùn)行時(shí),和腳本之間可以直接傳遞參數(shù),而不需要中間轉(zhuǎn)換。



          四、自定義數(shù)據(jù)結(jié)構(gòu)傳遞


          前文說(shuō)到,yaegi的一個(gè)極大的優(yōu)勢(shì),是可以直接傳遞自定義struct格式。


          這里,我先拋出如何傳遞自定義數(shù)據(jù)結(jié)構(gòu)的方法,然后再更進(jìn)一步講yaegi對(duì)第三方庫(kù)的支持。


          比如說(shuō),我定義了一個(gè)自定義的數(shù)據(jù)結(jié)構(gòu)(https://github.com/Andrew-M-C/go.util/blob/master/slice/lcs.go#L91),并且希望在Go腳本中進(jìn)行傳遞:


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


          那么,在對(duì)yaegi解釋器進(jìn)行初始化的時(shí)候,我們可以在intp變量初始化完成之后,調(diào)用以下代碼進(jìn)行符號(hào)表的初始化:


            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)), }, })


          這樣,腳本在調(diào)用的時(shí)候,除了原生庫(kù)之外,也可以使用 github.com/Andrew-M-C/go.util/slice中的Route結(jié)構(gòu)體。這就實(shí)現(xiàn)了struct的原生傳遞。


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



          五、Yaegi支持第三方庫(kù)


          (一)原理


          我們可以留意一下上文的例子中intp.Use(stdlib.Symbols) 這一句,這可以說(shuō)是yaegi區(qū)別于其他Go腳本庫(kù)的實(shí)現(xiàn)之一。這一句的含義是:使用標(biāo)準(zhǔn)庫(kù)的符號(hào)表。


          Yaegi解釋器分析了Go腳本的語(yǔ)法之后,會(huì)將其中的符號(hào)調(diào)用與符號(hào)表中的目標(biāo)進(jìn)行鏈接。而stdlib.Symbols就導(dǎo)出了Go中幾乎所有的標(biāo)準(zhǔn)庫(kù)的符號(hào)。不過(guò)從安全角度,yaegi禁止了諸如poweroff、reboot等的高權(quán)限系統(tǒng)調(diào)用。


          因此,我們自然而然地就可以想到,我們也可以把自定義的符號(hào)表定義進(jìn)去——這也就是Use函數(shù)的作用,將各符號(hào)的原型定義給yaegi就能夠?qū)崿F(xiàn)第三方庫(kù)的支持了。


          當(dāng)然,這種方法只能對(duì)腳本所能引用的第三方庫(kù)進(jìn)行預(yù)先定義,而不支持在腳本中動(dòng)態(tài)加載未定義的第三方庫(kù)。即便如此,這也極大地?cái)U(kuò)展了yaegi腳本的功能。



          (二)符號(hào)解析


          前文中,我們手動(dòng)在代碼中指定了需要引入的第三方符號(hào)表。但是對(duì)于很長(zhǎng)的代碼,一個(gè)符號(hào)一個(gè)符號(hào)地敲,實(shí)在是太麻煩了。其實(shí)yaegi提供了一個(gè)工具,能夠分析目標(biāo)package并輸出符號(hào)列表。我們可以看看yaegi的stdlib庫(kù)作為例子,它就是對(duì)Go原生的package文件進(jìn)行了解釋,并找到符號(hào)表,所使用的package就是yaegi附帶開(kāi)發(fā)的一個(gè)工具。


          因此,我們就可以借用這個(gè)功能,結(jié)合go generate,在代碼中動(dòng)態(tài)地生成符號(hào)表配置代碼。


          還是以上面的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


          工具會(huì)在當(dāng)前目錄下,生成一個(gè)github_com-Andrew-M-C-go_util-slice.go文件,文件的內(nèi)容就是符號(hào)表配置。這樣一來(lái),我們就不用費(fèi)時(shí)間去一個(gè)一個(gè)導(dǎo)出符號(hào)啦。



          六、與其他腳本方案的對(duì)比


          (一)功能對(duì)比


          我們?cè)谡{(diào)研了yaegi之外,也另外調(diào)研和對(duì)比了tengo和使用Lua的 gopher-lua。其中后者也是團(tuán)隊(duì)?wèi)?yīng)用得比較成熟的庫(kù)。


          筆者需要特別強(qiáng)調(diào)的是:tengo的標(biāo)題雖然說(shuō)自己用的是Go,但實(shí)際上是掛羊頭賣(mài)狗肉。tengo使用是自己的一套獨(dú)立語(yǔ)法,與官方Go完全不兼容,甚至乎連相似都稱不上。我們應(yīng)當(dāng)把它當(dāng)作另一種腳本語(yǔ)言來(lái)看。


          這三種方案的對(duì)比如下:



          總而言之:


          • gopher的優(yōu)勢(shì)在于性能。


          • yaegi的優(yōu)勢(shì)在于Go原生語(yǔ)法,以及可以接受的性能。


          • tengo的優(yōu)勢(shì)?對(duì)于筆者的這一使用場(chǎng)景來(lái)說(shuō),不存在的。


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


          • 它依然處于0.y.z版本的階段,也就是說(shuō)這只是beta版本,后續(xù)的API可能會(huì)有比較大的變化。


          • Go官方語(yǔ)法的大方向是支持泛型,而yaegi目前是不支持泛型的。后續(xù)需要關(guān)注yaegi在這方便的迭代情況



          (二)性能對(duì)比


          下文的表格比較多,這里先拋這三個(gè)庫(kù)的對(duì)比結(jié)論吧:


          • 從純算力性能上看,gopher擁有壓倒性的優(yōu)勢(shì)。


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


          • 非計(jì)算密集型的場(chǎng)景下,tengo的性能比較糟糕。平均場(chǎng)景也是最差的。


          • 簡(jiǎn)單的a+b


          這是一個(gè)簡(jiǎn)單的邏輯封裝,就是普通的res:=a+b,這是一個(gè)極限情況的測(cè)試。測(cè)試結(jié)果如下:



          結(jié)果讓人大跌眼鏡,對(duì)于特別簡(jiǎn)單的腳本,tengo的耗時(shí)極高,很可能是在進(jìn)入和退出tengo VM時(shí),消耗了過(guò)多的資源。


          而gopher則表現(xiàn)出了優(yōu)異的性能。讓人印象非常深刻。


          • 條件判斷


          該邏輯也很簡(jiǎn)單,判斷輸入數(shù)是否大于零。測(cè)試結(jié)果與簡(jiǎn)單加法類似,如下:



          • 斐波那契數(shù)


          前面兩個(gè)性能測(cè)試過(guò)于極限,只能作參考用。在tengo的README中,聲稱其擁有非常高的性能,可與gopher和原生Go相比,并且還能壓倒yaegi。既然tengo這么有信心,并且還給出了其使用的Fib函數(shù),那么我就來(lái)測(cè)一下。測(cè)試結(jié)果如下:




          七、工程應(yīng)用注意要點(diǎn)


          在實(shí)際工程應(yīng)用中,針對(duì)yaegi,筆者鎖定這樣的一個(gè)應(yīng)用場(chǎng)景:使用Go運(yùn)行時(shí)程序,調(diào)用Go腳本。我需要限制這個(gè)腳本完成有限的功能(比如數(shù)據(jù)檢查、過(guò)濾、清洗)。因此,我們應(yīng)該限制腳本可調(diào)用的能力。我們可以通過(guò)刪除stdlib.Symbols表中的部分package來(lái)實(shí)現(xiàn),筆者在實(shí)際應(yīng)用中,刪除了以下的package符號(hào):


          • os/xxx

          • net/xxx

          • log

          • io/xxx

          • database/xxx

          • runtime


          此外,雖然yaegi直接將腳本函數(shù)暴露出來(lái)可以直接調(diào)用,但是主程序不能對(duì)腳本的可靠性做任何的假設(shè)。換句話說(shuō),腳本可能會(huì)panic,或者是修改了主程序的變量,從而導(dǎo)致主程序panic。為了避免這一點(diǎn),我們要將腳本放在一個(gè)受限的環(huán)境里運(yùn)行,除了前面通過(guò)限制yaegi可調(diào)用的package的方式之外,還應(yīng)該限制調(diào)用腳本的方式。包括但不限于以下幾個(gè)手段:


          • 將調(diào)用邏輯放在獨(dú)立的goroutine中調(diào)用,并且通過(guò)recover函數(shù)捕獲異常。


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


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


          • 腳本的運(yùn)行時(shí)間也需要進(jìn)行限制,或者是監(jiān)控。如果腳本有bug出現(xiàn)了無(wú)限循環(huán),那么主調(diào)方應(yīng)能夠脫離這個(gè)腳本函數(shù),回到主流程中。


          當(dāng)然,文中充滿了對(duì)tengo的不推崇,也只是在筆者的這種使用場(chǎng)景下,tengo沒(méi)有任何優(yōu)勢(shì)而已,請(qǐng)讀者辯證閱讀,也歡迎補(bǔ)充和指正~


          ( 轉(zhuǎn)載須取得作者同意,未經(jīng)許可,禁止二次轉(zhuǎn)載 )



           作者簡(jiǎn)介


          張敏

          騰訊高級(jí)后臺(tái)工程師

          騰訊高級(jí)后臺(tái)工程師,在電子和互聯(lián)網(wǎng)行業(yè)深耕多年,擁有豐富的嵌入式和云服務(wù)后臺(tái)開(kāi)發(fā)經(jīng)驗(yàn),個(gè)人博客共有過(guò)百篇文章,云+社區(qū)Top50原創(chuàng)作者,技術(shù)創(chuàng)作101第二季講師,現(xiàn)負(fù)責(zé)騰訊產(chǎn)品后臺(tái)開(kāi)發(fā)。



          推薦閱讀


          福利

          我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 89
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色婷婷成人网站 | 成人精品午夜无码免费 | 污污污在线免费观看 | 欧美大屄视频 | 蜜臀久久精品久久久久久酒店 |