1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 5941字,需瀏覽 12分鐘

         ·

        2021-11-06 11:37


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


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


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


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


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


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


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


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


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


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


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



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


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


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


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


        • 可調(diào)用Go原生庫,并且可擴展第三方庫,進一步簡化邏輯。


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


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



        三、快速上手


        這里,我們寫一段最簡單的代碼,代碼的功能是斐波那契數(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)}


        令上方的代碼成為一個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{}) // 初始化一個 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變量,這直接就是一個函數(shù)變量。換句話說,yaegi直接將腳本中定義的函數(shù),解釋后向主調(diào)方程序直接暴露成同一結(jié)構(gòu)的函數(shù),調(diào)用方可以直接像調(diào)用普通函數(shù)一樣調(diào)用它,而不是像其他腳本庫一樣,需要調(diào)用一個專門的傳參函數(shù)、再獲得返回值、最后再將返回值進行轉(zhuǎn)換。


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



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


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


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


        比如說,我定義了一個自定義的數(shù)據(jù)結(jié)構(gòu)(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變量初始化完成之后,調(dià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)用的時候,除了原生庫之外,也可以使用 github.com/Andrew-M-C/go.util/slice中的Route結(jié)構(gòu)體。這就實現(xiàn)了struct的原生傳遞。


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



        五、Yaegi支持第三方庫


        (一)原理


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


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


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


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



        (二)符號解析


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


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


        還是以上面的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文件,文件的內(nèi)容就是符號表配置。這樣一來,我們就不用費時間去一個一個導(dǎo)出符號啦。



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


        (一)功能對比


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


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


        這三種方案的對比如下:



        總而言之:


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


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


        • tengo的優(yōu)勢?對于筆者的這一使用場景來說,不存在的。


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


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


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



        (二)性能對比


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


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


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


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


        • 簡單的a+b


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



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


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


        • 條件判斷


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



        • 斐波那契數(shù)


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




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


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


        • os/xxx

        • net/xxx

        • log

        • io/xxx

        • database/xxx

        • runtime


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


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


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


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


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


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


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



        ?作者簡介


        張敏

        騰訊高級后臺工程師

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



        ?推薦閱讀


        Node.js內(nèi)存泄漏的原因竟然是……?

        超詳細教程!手把手帶你使用Raft分布式共識性算法

        Pulsar與Rocketmq、Kafka、Inlong-TubeMQ,誰才是消息中間件的王者?

        gRPC如何在Golang和PHP中進行實戰(zhàn)?7步教你上手!






        ??閱讀原文前往「騰訊云+社區(qū)」作者個人主頁參與交流哦~

        瀏覽 69
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久久精品成人热国产成 | 成人做爱网 | 操jk美女| 女人18片毛片90分钟以上 | 青青草怡红院 | 欧性猛交ⅩXXX乱大交 | 一级黄色视频免费看 | 欧美乱操 | 欧美无乱码特黄日本大片 | 黑人上司好猛我好爽中文字幕 |