Go 語言中的外掛

zhuyaguang1368發表於2021-09-07

很多年以前我就開始寫一系列關於外掛的文章:介紹這些外掛在不同的系統和程式語言下是如何設計和實現的。今天這篇文章,我打算把這個系列擴充套件下,講講 Go 語言中一些外掛的例子。

需要提醒的是,本系列頭幾篇的文章就介紹了外掛的四個基本概念,並且宣告幾乎所有的外掛系統,都可以將它們的設計對映到以下 4 個概念來描述和理解:

  1. 發現

  2. 註冊

  3. 外掛附著到應用程式上的鉤子(又稱,” 掛載點 “)

  4. 將應用程式能力暴露給外掛(又稱,擴充套件 API)

Gopher holding an Ethernet cable plugged into the wall

兩種型別外掛

和其他靜態編譯程式語言一樣,Go 中通常會討論兩種一般型別的外掛:編譯時外掛和執行時外掛。這兩種我們都會講到。

編譯時外掛

編譯時外掛由一系列程式碼包組成,這些程式碼包編譯進了應用程式的二進位制檔案中。一旦二進位制檔案編譯好,它的功能就固定了。

最有名的 Go 編譯時外掛系統就是 database/sql 包的驅動程式。我已經寫了一整篇關於這個話題的文章,大家可以看下。

簡單概括下:資料庫驅動是主應用程式通過一個空白匯入 _ "name" 匯入的包。這些包通過它們的 init函式使用sql.Registerdatabase/sql註冊。

關於基本外掛的概念, 下面有一個編譯時外掛如何運作的例子(以database/sql為例)

  1. 發現:這點很明確,import一個外掛包。外掛可以在它們init函式自動執行註冊。
  2. 註冊:由於外掛被編譯到主應用程式之中,它可以直接從外掛中呼叫一個註冊函式 (例如 sql.Register)。
  3. 應用程式鉤子:通常,外掛將實現應用程式提供的介面,註冊過程將連線介面實現。外掛使用database/sql實現驅動程式。驅動程式介面和實現該介面的值將使用 sql.Register 註冊。
  4. 將應用程式能力暴露給外掛:對於編譯時外掛,這很簡單;由於外掛被編譯成二進位制檔案,它可以從主應用程式中匯入實用程式包,並根據需要在程式碼中使用它們。

執行時外掛

執行時外掛的程式碼不會被編譯到主應用程式的原始二進位制檔案中;相反,它在執行時連線到這個應用程式。在編譯語言中,實現這一目標的常用工具是共享庫。Go 也支援這種方法。本節的後面部分將提供一個使用共享庫,在 Go 中開發外掛系統的例子;最後還會討論其他方式實現的執行時外掛。

Go 自帶一個內建在標準庫中的外掛包。這個包讓我們可以寫出編譯進共享庫,而不是可執行二進位制檔案的 Go 程式。另外,它還提供了簡單函式來從外掛包裡面載入共享庫和獲取符號。

在這篇文章中,我開發了一個完整的執行時外掛系統示例;它複製了之前關於外掛基礎設施的文章中的htmlize原始碼,並且它的設計和後面那篇C 語言中的外掛文章類似。這個示例程式很簡單,就是把一些標記語言(比如 reStructuredText 或者 Markdown)轉換成 HTML,並支援外掛,使得我們能夠調整某些標記元素的處理方式。完整的示例程式碼在這篇文章裡。

Directory contents of the plugin sample

讓我們用外掛的基本概念來分析這個例子。

發現和註冊:是通過檔案系統查詢完成。主應用程式有一個帶有LoadPlugins函式的外掛包。這個函式掃描給定目錄中以.so 結尾的檔案,並將所有此類檔案視為外掛。它希望在每個共享庫中找到一個名為InitPlugin的全域性函式,並呼叫它,為它提供一個PluginManager(稍後會詳細介紹)。

外掛最開始是怎麼變成.so檔案的呢?通過 命令 -buildmode=plugin 構建。具體更多的細節,可以看示例原始碼 中的buildplugins.sh指令碼和 README 檔案。

應用程式勾子:現在是描述PluginManager型別的好時機。這是外掛和主應用程式之間通訊的主要型別。流程如下:

  • 應用程式在 LoadPlugins 新建一個 PluginManager,並將其傳給它找到的所有外掛。
  • 每個外掛使用PluginManager來給各種勾子註冊自己的處理程式。
  • LoadPlugins 在所有的外掛註冊後,將PluginManager返回給主程式。
  • 當應用程式執行時,使用  PluginManager 來根據需要呼叫已註冊外掛的勾子。

舉個例子,PluginManager 有下面這個函式:

func (pm *PluginManager) RegisterRoleHook(rolename string, hook RoleHook)

RoleHook 是一個函式型別:

// RoleHook takes the role contents, DB and Post and returns the text this role
// should be replaced with.
type RoleHook func(string, *content.DB, *content.Post) string

外掛可以呼叫RegisterRoleHook 來註冊一個特定文字角色的處理程式。請注意,儘管這個設計並沒有使用 Go 的 interfaces ,但是其他設計也可以實現同樣功能,取決於應用程式的具體情況。

將應用程式能力暴露給外掛:正如上面 RoleHook 型別那樣,應用程式將資料物件傳遞給外掛使用。content.DB 提供了對應用程式資料庫的訪問。content.Post 提供了當前格式化外掛的特定的 Post。外掛可以根據需要使用這些物件,來獲取應用程式的資料或者行為。

執行時外掛的替代方法

考慮到外掛包只是在 Go1.8 中新增的,還有前面描述的種種限制,所以也難怪 Go 生態系統中出現了其他外掛方法。

其中最有趣的一個方向就是,IMHO,通過 RPC 呼叫外掛。我一直很喜歡將應用程式解耦到獨立的程式中,然後通過 RPC 或本地主機上的 TCP 進行通訊。(我猜他們現在稱之為 微服務),因為它有幾個重要的優點:

  • 隔離性:外掛的崩潰不會導致整個應用程式崩潰。
  • 語言之間的互動性:如果 RPC 是介面,你還會在乎外掛使用什麼語言寫的嗎?
  • 分散式:如果外掛通過網路介面,我們可以很容易將它們分發到不同機器上,來提高效能、可靠性等等。

另外,Go 標準庫中有一個很強大的 RPC 包:net/rpc,讓這一點實現起來相當容易。

最廣泛使用的基於 RPC 的外掛系統就是hashicorp/go-plugin,Hashicorp 以建立優秀的 Go 軟體而聞名,顯然他們在許多系統中使用了 Go 外掛,因此這些外掛都是經過實戰測試過的。(儘管他們的文件可以寫的更好點)

Go 外掛執行在 net/rpc 之上,當然也支援 gRPC。像 gRPC 這樣的高階 RPC 協議非常適合外掛,因為它們包含了開箱即用的版本控制,解決了不同版本的外掛與主應用程式之間的互操作性問題。

更多原創文章乾貨分享,請關注公眾號
  • Go 語言中的外掛
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章