簡單來說,插件就是可以被其他軟件加載的軟件,通常用于擴展應用程序的功能和外觀,插件的開發人員甚至可以不直接修改基礎應用程序。
你很可能在日常生活中使用過插件,也許用的是其他名稱,如擴展(extensions)或附加組件(add-ons)。最常見的例子就是 VSCode 擴展,你應該用過 VSCode,對吧?畢竟這是最受程序員歡迎的文本編輯器。如果你用過,一定會同意 VSCode 本身就是一個文本編輯器,而不是集成開發環境。其基本功能非常簡單,幾乎不支持集成開發環境中常見的功能,如調試、自動完成和測試導航等。不過,通過編輯器的擴展市場,可以找到支持這些功能以及其他更多功能的各種插件。事實上,插件已成為編輯器的主要賣點之一,促使工具開發人員集中精力為編輯器制作專用插件,有時甚至超越了編碼本身的范疇,就像 Figma 所做的那樣[2]。
對于 VSCode 而言,插件是用 JavaScript 編寫的,但也有基于 Go 編寫插件的情況。例如 Terraform(云提供商基礎設施即代碼服務),它允許用戶為其工具編寫插件[3],從而與多個云供應商(AWS、GCP、Azure......)進行交互。
另一個例子是 API 網關服務 Kong,它允許開發人員使用不同語言(包括 Go)編寫插件,這些插件[4]可以在將請求轉發給底層服務之前,對接收到的請求進行處理。
我們將插件基礎架構分為三個部分:協議/API、實現和插件加載器。請注意,這種劃分不是官方標準,也不是紙上談兵,而是在實際應用中的通常做法。
協議是我們任意設置的定義和默認值,這樣就可以在各組件之間進行簡潔的通信。和任何協議一樣,需要設定插件和基礎應用程序之間的通信方式。為此,我們可以使用不同的方法,既可以通過簡單的文檔解釋期望的方法,也可以定義接口庫(編程接口,如 class foo implements bar)。只要插件的實現遵循這些準則,應用就能調用插件代碼。
我們需要編碼來實現協議設定的功能。也就是說,需要在插件代碼中實現預期的函數和變量,以便主應用程序可以調用。
提醒一下,插件代碼并不局限于這些實現方式。
這是需要由主應用程序執行的部分,有兩個職責:查找插件并在代碼中加載其功能。
插件是主程序項目的外部組件,因此需要一種方法來查找該程序的所有插件。我們可以簡單的在文件系統中定義一個固定的文件夾來存放所有插件,但最好是允許應用程序用戶通過配置文件來指向他們的插件,或者兩種方式同時支持。
安裝所有插件后,需要在應用程序中訪問它們的應用程序接口。這通常是通過鉤子實現的:運行時調用插件(或插件的一部分)的部分。以 VSCode 為例,"文件加載時"就是這樣一個鉤子,因此插件可以使用這個鉤子捕捉加載的文件并據此運行。實現哪些鉤子以及何時實現鉤子與應用程序的邏輯有內在聯系,只能具體問題具體分析。
學習編程的最佳方式莫過于動手實踐。因此我們來創建一個使用插件的簡單應用程序。
我們要構建的是一個基于插件的 HTTP 重定向服務。這是一個簡單的 HTTP 服務,監聽端口中的請求并將其重定向到另一個服務器,同時將響應傳遞給原始客戶端。有了這項服務,我們就可以接入請求并對其進行修改。在本例中,我們將通過插件獲取請求并打印。
至于插件加載部分,我們使用Go庫作為協議,并通過配置文件來定位插件。
我們首先定義插件協議。為此,我們定義一個 go 庫組件。
在定義該模塊之前,我們先定義應用程序組件:
# From a folder you want to keep the project:mkdir http-redirectcd http-redirectgo work initgo mod init github.com/<your_github_username>/http-redirectgo work use .
當然,你可以自行決定應用名稱。因為需要多個模塊進行交互,因此我們決定使用 go 工作區。要了解更多相關信息,請查看文檔[8]。
接下來可以創建庫組件了:
# From http-redirectmkdir protocolcd protocolgo mod init github.com/<your_github_username>/http-redirect/protocolgo work use . # Add new module to workspace
接下來創建一些文件,整個文件樹應該是這樣的:
我們將在 protocol.go 中開展工作。我們希望在協議中為每個請求調用函數。因此,我們要為插件實現一個名為 PreRequestHook 的函數,看起來是這樣的:
// protocol.gopackage protocolimport "net/http"http:// Plugins should export a variable called "Plugin" which implements this interfacetype HttpRedirectPlugin interface { PreRequestHook(*http.Request)}
代碼很簡單,我們只需獲取指向 http.Request 類型的指針(因為可能更改請求),然后將每個 HTTP 請求傳遞給我們的服務器。我們使用的是標準庫定義的類型,但請注意,也可以根據應用需求使用不同的類型。
就是這樣!但不要被例子的簡單性所迷惑。對于大型應用來說,這可能是一個相當大的文件,其中包含不同的接口、默認實現、配置和其他亂七八糟的東西。
現在有了一個可遵循的協議,就可以創建并實現插件了。
同樣,我們為插件創建一個新組件,并為其創建一個文件。
# From http-redirectmkdir log-plugincd log-plugingo mod init github.com/<your_github_username>/http-redirect/log-plugingo work use . # Add new module to workspacetouch plugin.go
現在的文件樹應該是這樣的:
我們來編寫插件!首先,創建一個函數來打印請求。
// log-plugin/plugin.gopackage mainimport ( "log/slog" "net/http" "net/http/httputil")func logRequest(req *http.Request) { result, err := httputil.DumpRequest(req, true) if err != nil { slog.Error("Failed to print request", "err", err) } slog.Info("Request sent:", "req", result)}func logRequestLikeCUrl(req *http.Request) { panic("Unimplemented!")}func main() { /*empty because it does nothing*/ }
這里的未實現函數只是為了顯示我們可以為更復雜的協議添加更多功能,只是目前還無法正確配置,因此不會使用。
我們要用到的是 logRequest 函數,它通過 go 標準庫的結構化日志組件打印請求。這就完成了我們的功能,但現在需要導出插件,使其滿足協議要求。
你可能注意到了,有一個什么也不做的 main 函數。這是 go 編譯器的要求,因為某些功能需要一個入口點。雖然這個編譯包中存在 main 函數,但不會作為可執行文件被調用。
我們需要導入庫。一般情況下,可以使用 go get 來恢復這個庫,但由于我們是在本地機器上開發,因此只需在 go.mod 文件中添加庫路徑即可:
replace github.com/profusion/http-redirect/protocol => ../protocol
接下來我們創建一個實現 HttpRedirectPlugin 接口的結構體,并調用日志函數。
// log-plugin/plugin.gopackage mainimport ( //… "github.com/<your_github_username>/http-redirect/protocol")// … previous code …type PluginStr struct{}// Compile time check for// PreRequestHook implements protocol.HttpRedirectPlugin.var _ protocol.HttpRedirectPlugin = PluginStr{}// PreRequestHook implements protocol.HttpRedirectPlugin.func (p PluginStr) PreRequestHook(req *http.Request) { logRequest(req)}var Plugin = PluginStr{}
這就是需要的所有代碼。我們只需將其作為插件構建即可。為此,我們只需向 go 編譯器傳遞 buildmode 標志:
# From http-redirect/log-plugingo build -buildmode=plugin -o plugin.so plugin.go
瞧!我們有了一個插件!現在只需將其加載到應用程序就行了。
我們需要一個應用程序來加載插件。這不是本文的重點,但以下是 Go 中 HTTP 重定向服務器代碼,我們可以對其進行修改。
// cmd/main.gopackage mainimport ( "flag" "fmt" "io" "log/slog" "net/http" "strings")var from intvar to stringfunc init() { flag.IntVar(&from, "from", 5555, "Local port to get requests") flag.StringVar(&to, "to", "", "Target server to redirect request to")}func main() { flag.Parse() Listen()}type proxy struct{}func Listen() { p := &proxy{} srvr := http.Server{ Addr: fmt.Sprintf(":%d", from), Handler: p, } if err := srvr.ListenAndServe(); err != nil { slog.Error("Server is down", "Error", err) }}// ServeHTTP implements http.Handler.func (p *proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // Remove original URL for redirect req.RequestURI = "" // Set URL accordingly req.URL.Host = to if req.TLS == nil { req.URL.Scheme = "http" } else { req.URL.Scheme = "https" } // Remove connection headers // (will be replaced by redirect client) DropHopHeaders(&req.Header) // Register Proxy Request SetProxyHeader(req) // Resend request client := &http.Client{} resp, err := client.Do(req) if err != nil { http.Error(rw, "Server Error: Redirect failed", http.StatusInternalServerError) } defer resp.Body.Close() // Once again, remove connection headers DropHopHeaders(&resp.Header) // Prepare and send response CopyHeaders(rw.Header(), &resp.Header) rw.WriteHeader(resp.StatusCode) if _, err = io.Copy(rw, resp.Body); err != nil { slog.Error("Error writing response", "error", err) }}func CopyHeaders(src http.Header, dst *http.Header) { for headingName, headingValues := range src { for _, value := range headingValues { dst.Add(headingName, value) } }}// Hop-by-hop headers. These are removed when sent to the backend.// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.htmlvar hopHeaders = []string{ "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", // canonicalized version of "TE" "Trailers", "Transfer-Encoding", "Upgrade",}func DropHopHeaders(head *http.Header) { for _, header := range hopHeaders { head.Del(header) }}func SetProxyHeader(req *http.Request) { headerName := "X-Forwarded-for" target := to if prior, ok := req.Header[headerName]; ok { // Not first proxy, append target = strings.Join(prior, ", ") + ", " + target } req.Header.Set(headerName, target)}
首先需要找到插件的位置。為此,我們將用 JSON 定義配置文件,在里面定義路徑列表,在本文中列表里只有一項,但請注意,這是一個為插件定義配置的機會。
// config.json[ "log-plugin/plugin.so"]
這就足夠了。然后我們編寫讀取該文件內容的代碼,為了保持整潔,將在另一個文件中進行插件加載。
// cmd/plugin.gopackage mainimport ( "encoding/json" "os")// global but private, safe usage here in this filevar pluginPathList []stringfunc LoadConfig() { f, err := os.ReadFile("config.json") if err != nil { // NOTE: in real cases, deal with this error panic(err) } json.Unmarshal(f, &pluginPathList)}
然后加載插件本身,為此我們將使用標準庫中的 golang 插件組件[9]。
// cmd/plugin.gopackage mainimport ( //… "plugin")// ...previous code...var pluginList []*plugin.Pluginfunc LoadPlugins() { // Allocate a list for storing all our plugins pluginList = make([]*plugin.Plugin, 0, len(pluginPathList)) for _, p := range pluginPathList { // We use plugin.Open to load the plugin by path plg, err := plugin.Open(p) if err != nil { // NOTE: in real cases, deal with this error panic(err) } pluginList = append(pluginList, plg) }}// Let's throw this here so it loads the plugins as soon as we import this modulefunc init() { LoadConfig() LoadPlugins()}
插件加載后,就可以訪問其符號了,包括我們在協議中定義的變量 Plugin。我們修改之前的代碼,保存這個變量,而不是整個插件。現在,我們的文件看起來是這樣的:
// cmd/plugin.goimport ( //… "protocol" "net/http")//…// Substitute previous codevar pluginList []*protocol.HttpRedirectPluginfunc LoadPlugins() { // Allocate a list for storing all our plugins pluginList = make([]*protocol.HttpRedirectPlugin, 0, len(pluginPathList)) for _, p := range pluginPathList { // We use plugin.Open to load plugins by path plg, err := plugin.Open(p) if err != nil { // NOTE: in real cases, deal with this error panic(err) } // Search for variable named "Plugin" v, err := plg.Lookup("Plugin") if err != nil { // NOTE: in real cases, deal with this error panic(err) } // Cast symbol to protocol type castV, ok := v.(protocol.HttpRedirectPlugin) if !ok { // NOTE: in real cases, deal with this error panic("Could not cast plugin") } pluginList = append(pluginList, &castV) }}// …
很好,現在 pluginList 中的所有變量都是正常的 golang 變量,可以直接訪問,就好像從一開始就是代碼的一部分。然后,我們構建鉤子函數,在發送請求前調用所有插件鉤子。
// cmd/plugin.go//…func PreRequestHook(req *http.Request) { for _, plg := range pluginList { // Plugin is a list of pointers, we need to dereference them // to use the proper function (*plg).PreRequestHook(req) }}
最后,在主代碼中調用鉤子:
// cmd/main.go//…// ServeHTTP implements http.Handler.func (p *proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { PreRequestHook(req)// …
就是這樣!我們創建了一個應用程序和一個插件,將插件加載到應用中,然后針對收到的每個請求運行插件代碼,并記錄這些請求。
想要測試?直接運行就行:
# From http-redirectgo run cmd/*.go -from <port> -to <url>
我們在本文中討論了什么是插件、插件的用途,以及如何基于 Go 標準庫創建支持插件的應用程序的能力。在未來的工作中,請考慮通過這種基礎架構為解決方案提供更好的可擴展性,從而幫助其他開發人員可以更廣泛的使用我們的工具和應用。
本文鏈接:http://www.tebozhan.com/showinfo-26-94846-0.html如何基于 Golang 標準庫實現插件功能
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 提高數值精度:掌握 C++ 中的 setprecision
下一篇: 為什么高手都要用非阻塞IO?