Golang插件系统实现

插件可以在解耦的基础上灵活扩展应用功能,本文介绍了如何基于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.mzph.cn/news/824598.shtml

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

相关文章

[入门]测试层级-ApiHug准备-测试篇-005

&#x1f917; ApiHug {Postman|Swagger|Api...} 快↑ 准√ 省↓ GitHub - apihug/apihug.com: All abou the Apihug apihug.com: 有爱&#xff0c;有温度&#xff0c;有质量&#xff0c;有信任ApiHug - API design Copilot - IntelliJ IDEs Plugin | Marketplace 这里的测…

学习STM32第十五天

SPI外设 一、简介 STM32F4XX内部集成硬件SPI收发电路&#xff0c;可以由硬件自动执行时钟生成、数据收发等功能&#xff0c;减轻CPU负担&#xff0c;可配置8位/16位数据帧&#xff0c;高位&#xff08;最常用&#xff09;/低位先行&#xff0c;三组SPI接口&#xff0c;支持DMA…

第一篇【传奇开心果系列】我和AI面对面聊编程:深度比较PyQt5和tkinter.ttk

传奇开心果系列博文 系列博文目录我和AI面对面聊编程系列 博文目录前言一、今天我们面对广大读者选择PyQt5和tkinter.ttk做比较这个话题目的是什么&#xff1f;二、举一个最简单的pyqt5信号和插槽的例子三、这和tkinter的点击事件有什么区别&#xff1f;四、如何选择&#xff1…

MySQL Explan执行计划详解

Explan执行计划 首先我们采用explan执行计划 执行一条sql&#xff0c;发现返回了12个列&#xff0c;下面会详细解释每一列 1、ID列 id列的值是代表了select语句执行顺序&#xff0c;是和select相关联的&#xff1b;id列的值大的会优先执行&#xff0c;如果id列为空最后执行&a…

数据库的创建

数据库分类 通过查看对象资源管理器来区分数据库类型 数据库物理文件的组成 : 数据库文件 日志文件 创建一个主数据文件和一个日志文件

上线流程及操作

上节回顾 1 搜索功能-前端&#xff1a;搜索框&#xff0c;搜索结果页面-后端&#xff1a;一种类型课程-APIResponse(actual_courseres.data.get(results),free_course[],light_course[])-搜索&#xff0c;如果数据量很大&#xff0c;直接使用mysql&#xff0c;效率非常低--》E…

淘宝商品数据抓取新策略:API接口助力获取标题、分类与店铺名

随着电子商务的迅猛发展&#xff0c;淘宝作为中国最大的网络购物平台&#xff0c;其商品数据对于众多商家、研究者和市场分析师来说具有极高的价值。然而&#xff0c;如何高效、准确地抓取淘宝商品数据&#xff0c;尤其是商品标题、分类和店铺名等关键信息&#xff0c;一直是一…

nginx部署上线

1. windows配置nginx 打包命令 npm run build:prod 1. 安装 nginx mac windows 2. mac / windows 环境下ngnix部署启动项目 2. nginx 解决 history 的 404 问题 3. nginx配置代理解决生产环境跨域问题

极速、易用、高度定制化的开源社区交流平台:Flarum

Flarum&#xff1a;轻盈高效&#xff0c;引领未来社区互动新风尚的革命性论坛平台- 精选真开源&#xff0c;释放新价值。 概览 Flarum是一款精心打造的现代网站讨论平台&#xff0c;以其精炼高效而著称。作为 esoTalk 和 FluxBB 的理念和技术的集大成者&#xff0c;Flarum 致力…

Ubuntu 20.04 LTS 在3588安卓主板上测试yolov8-1.0版本的yolov8n-seg模型

0. 创建虚拟环境 #!< 创建虚拟环境yolov8 $ sudo pip install virtualenv $ sudo pip install virtualenvwrapper $ mkvirtualenv yolov8 -p /usr/bin/python3.81. 将yolov8n-seg.pt转换为yolov8n-seg.onnx文件 #!< 创建项目目录yolov8-rknn并下载yolov8n-seg.pt模型文…

虚拟机vm桥接模式linux(centos,ubuntu)联网

台式机网线 查看宿主机网络 编辑虚拟机—>虚拟网络编辑器–>更改设置 选择&#xff0c;确定 进入linux系统 输入ip addr找到自己的网卡 我的是eno16777736 centos&#xff1a; 编辑 HWADDR"00:0C:29:54:CE:B8" TYPE"Ethernet" BOOTPROTO"…

公网IP多少钱可以购买?

公网IP是指可以在全球范围内访问和识别的唯一IP地址。对于许多企业和个人用户来说&#xff0c;公网IP是实现远程访问、搭建服务器、建立安全连接等重要需求的基础。公网IP的获取并不是免费的&#xff0c;并且价格因供应商和地区而异。 现有公网IP市场 当前&#xff0c;市场上有…

博弈论和sg函数

Nim游戏 题目链接&#xff1a;Nim游戏 先说结论&#xff1a;假设n堆石子&#xff0c;石子数分别为a1,a2,a3.....&#xff0c;则当a1^a2^a3^...^an0时先手必败&#xff0c;否则先手必胜。 因为所表示的二进制位必定是成对出现的&#xff0c;根据性质 1 ^ 1 0 &#xff0c;0 …

深入理解 pytest Fixture 方法及其应用

当涉及到编写自动化测试时&#xff0c;测试框架和工具的选择对于测试用例的设计和执行非常重要。在Python 中&#xff0c;pytest是一种广泛使用的测试框架&#xff0c;它提供了丰富的功能和灵活的扩展性。其中一个很有用的功 能是fixture方法&#xff0c;它允许我们初始化测试环…

HTML5漫画风格个人介绍源码

源码介绍 HTML5漫画风格个人介绍源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;重定向这个界面 效果截图 源码下载 HTML5漫画风格…

设计模式———单例模式

单例也就是只能有一个实例&#xff0c;即只创建一个实例对象&#xff0c;不能有多个。 可能会疑惑&#xff0c;那我写代码的时候注意点&#xff0c;只new一次不就得了。理论上是可以的&#xff0c;但在实际中很难实现&#xff0c;因为你无法预料到后面是否会脑抽一下~~因此我们…

【Pytorch】Conv1d

conv1d 先看看官方文档 再来个简单的例子 import torch import numpy as np import torch.nn as nndata np.arange(1, 13).reshape([1, 4, 3]) data torch.tensor(data, dtypetorch.float) print("[data]:\n", data) conv nn.Conv1d(in_channels4, out_channels1…

启明智显应用分享|基于ESP32-S3方案的SC01PLUS彩屏与chatgpt融合应用DEMO

今天将带大家真实体验科技与智慧的完美融合——SC01PLUS与ChatGPT的深度融合DEMO效果呈现。 彩屏的清晰显示与ChatGPT的精准回答&#xff0c;将为我们带来前所未有的便捷与高效。 SC01PLUS是启明智显基于ESP32-S3打造的一款3.5寸480*320分辨率的彩屏产品&#xff0c;您可以看…

【Git】git命令大全(持续更新)

本文架构 0.描述git简介术语 1.常用命令2. 信息管理新建git库命令更改存在库设置获取当前库信息 3.工作空间相关将工作空间文件添加到缓存区&#xff08;增&#xff09;从工作空间中移除文件&#xff08;删&#xff09;撤销提交 4.远程仓库相关同步远程仓库分支 &#xff08;持…

高版本Android studio 使用Markdown无法预览(已解决)

目录 概述 解决方法 概述 本人升级Android studio 当前版本为Android Studio Jellyfish | 2023.3.1 RC 2导致Markdown无法预览。 我尝试了很多网上的方法都无法Markdown解决预览问题&#xff0c;包括升级插件、安装各种和Markdown相关的插件及使用“Choose Boot Java Runtim…