Golang插件系统实现

news2024/11/28 11:01:17

插件可以在解耦的基础上灵活扩展应用功能,本文介绍了如何基于Golang标准库实现插件功能,帮助我们构建更灵活可扩展的应用。原文: Plugins with Go

alt
什么是插件

简单来说,插件就是可以被其他软件加载的软件,通常用于扩展应用程序的功能和外观,插件的开发人员甚至可以不直接修改基础应用程序。

你很可能在日常生活中使用过插件,也许用的是其他名称,如扩展(extensions)或附加组件(add-ons)。最常见的例子就是 VSCode 扩展,你应该用过 VSCode,对吧?毕竟这是最受程序员欢迎的文本编辑器。如果你用过,一定会同意 VSCode 本身就是一个文本编辑器,而不是集成开发环境。其基本功能非常简单,几乎不支持集成开发环境中常见的功能,如调试、自动完成和测试导航等。不过,通过编辑器的扩展市场,可以找到支持这些功能以及其他更多功能的各种插件。事实上,插件已成为编辑器的主要卖点之一,促使工具开发人员集中精力为编辑器制作专用插件,有时甚至超越了编码本身的范畴,就像 Figma 所做的那样[1]

对于 VSCode 而言,插件是用 JavaScript 编写的,但也有基于 Go 编写插件的情况。例如 Terraform(云提供商基础设施即代码服务),它允许用户为其工具编写插件[2],从而与多个云供应商(AWS、GCP、Azure......)进行交互。

另一个例子是 API 网关服务 Kong,它允许开发人员使用不同语言(包括 Go)编写插件,这些插件[3]可以在将请求转发给底层服务之前,对接收到的请求进行处理。

免责声明
  • 本文假设你至少对 Go 语言有基本的了解。如果还不了解,建议先 了解一下 Go [4],然后再来阅读。
  • 本文示例代码中的某些功能要求至少使用 Go 1.21.0 版本。
  • Windows 机器尚未支持 Go 的插件功能。如果你用的是 Windows,建议使用 WSL [5]
  • 本文生成的代码可在 Github 代码库 [6]中找到。
插件的基础设施

我们将插件基础架构分为三个部分:协议/API实现插件加载器。请注意,这种划分不是官方标准,也不是纸上谈兵,而是在实际应用中的通常做法。

alt
协议/API

协议是我们任意设置的定义和默认值,这样就可以在各组件之间进行简洁的通信。和任何协议一样,需要设定插件和基础应用程序之间的通信方式。为此,我们可以使用不同的方法,既可以通过简单的文档解释期望的方法,也可以定义接口库(编程接口,如 class foo implements bar)。只要插件的实现遵循这些准则,应用就能调用插件代码。

实现

我们需要编码来实现协议设定的功能。也就是说,需要在插件代码中实现预期的函数和变量,以便主应用程序可以调用。

提醒一下,插件代码并不局限于这些实现方式。

插件加载器

这是需要由主应用程序执行的部分,有两个职责:查找插件并在代码中加载其功能。

插件是主程序项目的外部组件,因此需要一种方法来查找该程序的所有插件。我们可以简单的在文件系统中定义一个固定的文件夹来存放所有插件,但最好是允许应用程序用户通过配置文件来指向他们的插件,或者两种方式同时支持。

安装所有插件后,需要在应用程序中访问它们的应用程序接口。这通常是通过钩子实现的:运行时调用插件(或插件的一部分)的部分。以 VSCode 为例,"文件加载时"就是这样一个钩子,因此插件可以使用这个钩子捕捉加载的文件并据此运行。实现哪些钩子以及何时实现钩子与应用程序的逻辑有内在联系,只能具体问题具体分析。

我们在构建什么

学习编程的最佳方式莫过于动手实践。因此我们来创建一个使用插件的简单应用程序。

我们要构建的是一个基于插件的 HTTP 重定向服务。这是一个简单的 HTTP 服务,监听端口中的请求并将其重定向到另一个服务器,同时将响应传递给原始客户端。有了这项服务,我们就可以接入请求并对其进行修改。在本例中,我们将通过插件获取请求并打印。

至于插件加载部分,我们使用Go库作为协议,并通过配置文件来定位插件。

开发插件协议

我们首先定义插件协议。为此,我们定义一个 go 库组件。

在定义该模块之前,我们先定义应用程序组件:

# From a folder you want to keep the project:
mkdir http-redirect
cd http-redirect
go work init
go mod init github.com/<your_github_username>/http-redirect
go work use .

当然,你可以自行决定应用名称。因为需要多个模块进行交互,因此我们决定使用 go 工作区。要了解更多相关信息,请查看文档[7]

接下来可以创建库组件了:

# From http-redirect
mkdir protocol
cd protocol
go mod init github.com/<your_github_username>/http-redirect/protocol
go work use . # Add new module to workspace

接下来创建一些文件,整个文件树应该是这样的:

alt

我们将在 protocol.go 中开展工作。我们希望在协议中为每个请求调用函数。因此,我们要为插件实现一个名为 PreRequestHook 的函数,看起来是这样的:

// protocol.go
package protocol

import "net/http"

// Plugins should export a variable called "Plugin" which implements this interface
type HttpRedirectPlugin interface {
  PreRequestHook(*http.Request)
}

代码很简单,我们只需获取指向 http.Request 类型的指针(因为可能更改请求),然后将每个 HTTP 请求传递给我们的服务器。我们使用的是标准库定义的类型,但请注意,也可以根据应用需求使用不同的类型。

就是这样!但不要被例子的简单性所迷惑。对于大型应用来说,这可能是一个相当大的文件,其中包含不同的接口、默认实现、配置和其他乱七八糟的东西。

实现插件

现在有了一个可遵循的协议,就可以创建并实现插件了。

同样,我们为插件创建一个新组件,并为其创建一个文件。

# From http-redirect
mkdir log-plugin
cd log-plugin
go mod init github.com/<your_github_username>/http-redirect/log-plugin
go work use . # Add new module to workspace
touch plugin.go

现在的文件树应该是这样的:

alt

我们来编写插件!首先,创建一个函数来打印请求。

// log-plugin/plugin.go
package main

import (
  "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.go
package main

import (
  //…
  "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-plugin
go build -buildmode=plugin -o plugin.so plugin.go

瞧!我们有了一个插件!现在只需将其加载到应用程序就行了。

加载插件

我们需要一个应用程序来加载插件。这不是本文的重点,但以下是 Go 中 HTTP 重定向服务器代码,我们可以对其进行修改。

// cmd/main.go
package main

import (
  "flag"
  "fmt"
  "io"
  "log/slog"
  "net/http"
  "strings"
)

var from int
var to string

func 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.html
var 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.go
package main

import (
  "encoding/json"
  "os"
)

// global but private, safe usage here in this file
var pluginPathList []string

func 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 插件组件[8]

// cmd/plugin.go
package main

import (
  //…
  "plugin"
)

// ...previous code...

var pluginList []*plugin.Plugin

func LoadPlugins() {
  // Allocate a list for storing all our plugins
  pluginList = make([]*plugin.Plugin, 0len(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 module
func init() {
  LoadConfig()
  LoadPlugins()
}

插件加载后,就可以访问其符号了,包括我们在协议中定义的变量 Plugin。我们修改之前的代码,保存这个变量,而不是整个插件。现在,我们的文件看起来是这样的:

// cmd/plugin.go

import (
  //…
  "protocol"
  "net/http"
)

//…

// Substitute previous code
var pluginList []*protocol.HttpRedirectPlugin

func LoadPlugins() {
  // Allocate a list for storing all our plugins
  pluginList = make([]*protocol.HttpRedirectPlugin, 0len(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-redirect
go run cmd/*.go -from <port> -to <url>
alt
结论

我们在本文中讨论了什么是插件、插件的用途,以及如何基于 Go 标准库创建支持插件的应用程序的能力。在未来的工作中,请考虑通过这种基础架构为解决方案提供更好的可扩展性,从而帮助其他开发人员可以更广泛的使用我们的工具和应用。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

参考资料
[1]

Figma for VS Code: https://help.figma.com/hc/en-us/articles/15023121296151-Figma-for-VS-Code

[2]

Terraform plugin: https://developer.hashicorp.com/terraform/plugin

[3]

Kong hub: https://docs.konghq.com/hub/

[4]

Go Tour: https://go.dev/tour/welcome/1

[5]

WSL Install: https://learn.microsoft.com/en-us/windows/wsl/install

[6]

HTTP redirect sample code: https://github.com/profusion/http-redirect-pf-blog-post

[7]

Go Workspaces: https://go.dev/doc/tutorial/workspaces

[8]

Go stdlib plugin: https://pkg.go.dev/plugin

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1603597.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式学习56-ARM5(linux驱动启动程序)

知识零碎&#xff1a; bootm&#xff1a; 启动内核同时给内核传参 …

VAR:自回归家族文生图新SOTA,ImageNet上超越Diffusion与DiTs

一、背景&#xff1a; 在人工智能领域&#xff0c;尤其是计算机视觉和自然语言处理中&#xff0c;自回归&#xff08;AR&#xff09;大型模型&#xff08;如GPT系列&#xff09;因其强大的生成能力和在多种任务上的通用性而受到广泛关注。这些模型通过自监督学习策略&#xff0…

Android 性能优化(七):APK安装包体积优化

包体积优化重要性 移动 App 特别关注投放转化率指标&#xff0c;而 App 包体积是影响用户新增的重要因素&#xff0c;而 App 的包体积又是影响投放转化率的重要因素。 Google 2016 年公布的研究报告显示&#xff0c;包体积每上升 6MB 就会带来下载转化率降低 1%&#xff0c; …

获取公募基金净值【数据分析系列博文】

摘要 从指定网址获取公募基金净值数据&#xff0c;快速解析并存储数据。 &#xff08;该博文针对自由学习者获取数据&#xff1b;而在投顾、基金、证券等公司&#xff0c;通常有Wind、聚源、通联等厂商采购的数据&#xff09; 导入所需的库&#xff1a;代码导入了一些常用的库…

CSS3 伪元素与伪类选择器区别、详解与应用实例

伪元素与伪类两者都是通过在选择器后附加一个特定的关键字来定义&#xff0c;遵循相似的语法规则&#xff0c;并在 CSS 规则块中设置相应的样式。伪元素 能够通过 content 属性添加或替换内容。例如&#xff0c;:before 和 :after 可以插入文本、图像或其他生成的内容。伪类 仅…

Go 单元测试之Mysql数据库集成测试

文章目录 一、 sqlmock介绍二、安装三、基本用法四、一个小案例五、Gorm 初始化注意点 一、 sqlmock介绍 sqlmock 是一个用于测试数据库交互的 Go 模拟库。它可以模拟 SQL 查询、插入、更新等操作&#xff0c;并且可以验证 SQL 语句的执行情况&#xff0c;非常适合用于单元测试…

数据赋能(58)——要求:数据赋能实施部门能力

“要求&#xff1a;数据赋能实施部门能力”是作为标准的参考内容编写的。 在实施数据赋能中&#xff0c;数据赋能实施部门的能力体现在多个方面&#xff0c;关键能力如下图所示。 在实施数据赋能的过程中&#xff0c;数据赋能实施部门应具备的关键能力如下。 理性思维与逻辑分…

ES源码四:网络通信层流程

听说ES网络层很难&#xff1f;今天来卷它&#x1f604; 前言 ES网络层比较复杂&#xff0c;分为两个部分&#xff1a; 基于HTTP协议的REST服务端基于TCP实现的PRC框架 插件化设计的网络层模块&#xff08;NetworkModule&#xff09; 入口还是上一章的创建Node构造方法的地方…

如何爬出 Kotlin 协程死锁的坑?

作者&#xff1a;悬衡 一、前言 在 Java 中有一个非常经典的死锁问题, 就是明明自己已经占用了线程池, 却还继续去申请它, 自己等自己, 就死锁了, 如下图和代码: // 这段代码将死锁到天荒地老final ExecutorService executorService Executors.newSingleThreadExecutor();exe…

4月27日复旦大学-华盛顿大学EMBA项目教授分享暨招生说明会

复旦大学-华盛顿大学EMBA项目2024年4月27日精彩看点&#xff1a;      国际视野助推商业价值&#xff0c;“她力量”赋能管理      数字营销浪潮中&#xff0c;如何重新定义价值创造      主讲嘉宾    复旦大学-华盛顿大学EMBA项目教授分享暨招生说明会     …

Redmi Turbo 3新品发布,天星金融(原小米金融)优惠加持护航新机体验

Redmi新十年使命不变&#xff0c;挑战不断升级。Redmi Turbo 3&#xff0c;作为Turbo系列的开篇之作&#xff0c;将自身定位为新生代性能旗舰&#xff0c;决心重塑中端性能新格局。据悉&#xff0c;Redmi Turbo 3于4月10日已正式发布。预售期间更是连续数日&#xff0c;蝉联小米…

C++:文件内容完全读入

在上一篇文章中我留下了一点小坑&#xff1a;使用>> 运算符&#xff0c;这个运算符默认将空格作为分隔符&#xff0c;所以在文件内容读取的时候发现在读到空格时就会停止读取&#xff0c;导致读取内容不完整&#xff0c;这显然不符合日常的使用用能&#xff0c;那么今天就…

科学突破可能开创6G通信新时代

格拉斯哥大学开发的火柴盒大小的天线可以为全息通话、改进自动驾驶和更好的医疗保健的世界铺平道路。 格拉斯哥大学表示&#xff0c;这种创新的无线通信天线将超材料的独特特性与复杂的信号处理相结合&#xff0c;有助于构建未来的 6G 网络。 数字编码动态超表面天线&#xf…

python-flask结合bootstrap实现网页小工具实例-半小时速通版

参考&#xff1a; Python之flask结合Bootstrap框架快速搭建Web应用_支持bootstrap的python软件-CSDN博客 https://blog.csdn.net/lovedingd/article/details/106696832 Bootstrap 警告框 | 菜鸟教程 https://www.runoob.com/bootstrap/bootstrap-alert-plugin.html flask框架…

机器学习实验二-----决策树构建

决策树是机器学习中一种基本的分类和回归算法&#xff0c;是依托于策略抉择而建立起来的树。本文学习的是决策树的分类 1. 构建决策树流程 选择算法&#xff1a;常用的算法包括ID3、C4.5、CART等。 划分节点&#xff1a;根据数据特征和算法选择&#xff0c;递归地划分节点&…

C++算法题 - 矩阵

目录 36. 有效的数独54. 螺旋矩阵48. 旋转图像73. 矩阵置零289. 生命游戏 36. 有效的数独 LeetCode_link 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现…

Linux安装和使用Android Debug Bridge(ADB)

目录 1、开发环境和工具 2、ADB是什么&#xff1f; 3、安装ADB 3.1、使用包管理器安装 ADB 3.2、手动安装 ADB 4、使用ADB 4.1、连接设备 4.2、执行shell命令 4.3、安装应用程序 4.4、截取屏幕截图 4.5、模拟按键和手势 4.6、上传文件到Android设备 4.7、从Android设备下载文件…

el-table 表格列里添加 树

<el-table-column label"部门名称" align"center"><template slot-scope"scope"><el-cascader filterable :disabled"type 3 ? true : false" :show-all-levels"false":ref"provinceTree scope.row.…

【机器学习300问】72、神经网络的隐藏层数量和各层神经元节点数如何影响模型的表现?

评估深度学习的模型的性能依旧可以用偏差和方差来衡量。它们反映了模型在预测过程中与理想情况的偏离程度&#xff0c;以及模型对数据扰动的敏感性。我们简单回顾一下什么是模型的偏差和方差&#xff1f; 一、深度学习模型的偏差和方差 偏差&#xff1a;衡量模型预测结果的期望…

[Meachines][Easy] Usage

Main # nmap -sV -sC 10.10.11.18 --min-rate 1000 # echo 10.10.11.18 usage.htb admin.usage.htb >> /etc/hosts 在/forget-password发现存在SQL注入 emailmaptnh%40log.comAND5212%3dBENCHMARK(5000000,MD5(0x62434473))--NKGG $ sqlmap -r request.txt --level 5 -…