Go 插件系統,一個涼了快半截的特性?
本文轉載自微信公眾號「腦子進煎魚了」,作者陳煎魚。轉載本文請聯系腦子進煎魚了公眾號。
大家好,我是煎魚。
在 Go 語言中,有一個好像很好用,但卻比較少人提及的功能,那就是 Go Plugin。
目前在 Go 工程中普遍還沒廣泛的使用起來,覆蓋率不高。在 Go issues 上吐槽挺多,甚至感覺有點像涼了半截的樣子。
前段時間小咸魚的同事問了他這功能怎么用,他正想甩出一個鏈接,但發現...煎魚竟然沒寫過,這不,Go 知識板塊的文章地圖得補全。
今天煎魚就大家一起學習 Go Plugin,看看為什么會說感覺 “有點像涼了半截” 的樣子,打開來看看這個問題在哪。
是什么
Go Team 最早在 Go1.7 實驗,在 Go1.8 正式引入了 Go Plugin 的機制。于 2016 年發布,一開始僅支持 Linux 實現:
Go Plugin 機制實現了 Go 插件的加載和符號解析,能夠支持將我們所編寫的 Go 包編譯為共享庫(.so)。
這樣 Go 工程就可以加載所編譯好的 Go Plugin(已經變成了共享庫文件),在程序中調用共享庫中的函數、常量、變量等使用。也稱其為 Go 語言中的熱插拔的插件系統。
截止 Go1.17 為止,Go Plugin 僅支持在 Linux、FreeBSD 和 MacOS 上運行,還不支持 Windows。
為什么需要
Go 語言是靜態語言,正常我們寫一個程序,分如下兩個角度來看:
- 從代碼編寫的角度來看:我們在程序編寫的時候就已經把所有的功能實現給確定了,不會發生什么根本性的變化。
- 從程序的角度來看:在 Go 進行編譯時,就已經把所有引用的標準庫、第三方庫等都編譯打包好進二進制文件了,因此也就無法在運行時去動態加載,所以沒法有其它的可能性。
那么為什么需要 Go Plugin 呢,原因如下:
- 可插拔的插件:程序能夠隨時的安裝插件,也能夠卸載他,獲得更多運行時的自定義能力。
- 可動態加載運行時模塊:隨時安裝了插件,自然也就需要可自行決定運行哪個插件的模塊了。
- 可獨立開發插件、模塊:主系統和子插件,可能由不同的團隊開發和提供,也更有價值。
其實本質上還是希望程序能夠在運行時實現動態的外部加載,根據不同的條件、場景加載不同的插件功能。
使用方法
通用概念
Go 官方給出的例子非常簡單,只需要在 Go 編譯時指定為插件就可以了。
編譯的命令例子如下:
- go build -buildmode=plugin
當一個插件初次被打開時,所有尚未成為程序一部分的包的init函數被調用。不過主函數不被運行。需要注意一個插件只會被初始化一次,插件不能被關閉。
其共有如下幾個 API:
- type Plugin
- func Open(path string) (*Plugin, error)
- func (p *Plugin) Lookup(symName string) (Symbol, error)
- type Symbol
- Plugin.Open:開啟一個 Go 插件。如果一個路徑已經被打開,那么將返回現有的 *Plugin。
- Plugin.Lookup:在插件中搜索名所傳入的符號,符號是任何導出的變量或函數。如果沒有找到該符號,它會報告一個錯誤。
主要就是細分為插件和符號,符號(Symbol)本身是一個 interface,在調用 Plugin 相關方法后還是需要進一步斷言才能使用。
實際編寫
了解基本定義后,我們定義一個插件,一般我們會有個 plugins/ 的目錄,作為主程序的附屬插件集。
插件的代碼如下:
- package main
- import "fmt"
- var V int
- func F() {
- fmt.Printf("腦子進了 %d 次煎魚 \n", V)
- }
包名必須為 main,在該插件根目錄運行:
- go build -buildmode=plugin -o plugin.so main.go
就可以看到在編譯的目錄下多出了 plugin.so 文件,這就是這個插件經過編譯后的動態庫 .so 文件。
隨后只需在主程序加載這個插件就可以了,如下:
- import (
- "plugin"
- )
- func main() {
- p, err := plugin.Open("plugin.so")
- if err != nil {
- panic(err)
- }
- v, err := p.Lookup("V")
- if err != nil {
- panic(err)
- }
- f, err := p.Lookup("F")
- if err != nil {
- panic(err)
- }
- *v.(*int) = 999
- f.(func())()
- }
輸出結果:
- 腦子進了 999 次煎魚
在程序中,我們先調用了 plugin.Open 方法打開了前面所編譯的 plugin.so 動態庫。
緊接著調用 plugin.Lookup 方法,定位到了變量 V 和 方法 F,但由于其返回值都是 Symbol(interface),因此我們需要對其進行類型斷言,隨時才可以調用和使用。
至此完成了一個插件的基本使用。
為什么不被需要
在前面我們提到了大量 Go Plugin 的優點,也演示了其 Plugin 代碼編寫起來有多么的簡單和方便。
但,為什么 Go Plugin 已經發布了 4 年依然沒有被大規模應用,甚至對于不少業務開發來講是不被需要的呢,或是壓根不知道有這東西?
究其原因,我個人認為一個東西的廣泛應用要至少符合以下三大點:
- 基數:需要的場景多。
- 上手:方便且易用。
- 質量:沒有大問題。
比較折騰的人的是,Go Plugin 這三大點都欠一些火候,綜合導致了該功能的沒有大規模應用。
像是要應用 Go Plugin 有諸如下約束:
- 環境問題:不支持 Windows 等(暫無計劃,#19282),MacOS 有些問題,一開始只支持 Linux,其他的也是后面慢慢增加的支持。
- Go 版本問題:Plugin 構建環境和第三方包的依賴版本需要保持一致。
- 特性問題:Plugin 特性的缺失,例如不支持插件的關閉,暫時無新計劃支持(#20461)。
總結
在 Go issues 中暢游時,能看到許多小伙伴在以往 4 年踩過的坑和無奈。甚至有一個高贊回答(#19282)表示:插件功能主要是一個技術演示,由于一些不道德的原因,被作為語言的穩定功能發布(The plugin feature is mostly a tech demo that for some unholy reason got released as a stable feature of the language.)。
目前 Go Plugin 并不是 Go Team 的優先事項,在 Windows/Mac 的支持存在問題。GOPATH 有問題,不同 GO 版本也有問題。更是建議如果您想要插件,請走較慢的 grpc 路線,因為它們是有效的插件。
也可以參考為數不多的一些 Go Plugin 用戶的方案,例如:tidb,甚至寫了個指導文檔。
但如果要在生產正式使用,勸你還是需要慎重考慮,又或是再等等...等更完善的那一天?
參考
Go Package plugin
Why is there no windows support for plugins?
plugin: add Windows support
plugin: Add support for closing plugins
如何評價 Go 標準庫中新增的 plugin 包?
一文搞懂Go語言的plugin