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 多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据库的特点

前面讲了&#xff0c;数据库是有组织的&#xff0c;规范的把数据保存起来的。 怎么个组织的&#xff0c;规范的&#xff1f; 数据库的特点&#xff1a; 1.将数据放到数据表格&#xff08;二维表&#xff09;中&#xff0c;在将表格放到库中。 2.一个数据库中可以有多张表&am…

【.Net动态Web API】背景与实现原理

&#x1f680;前言 本文是《.Net Core进阶编程课程》教程专栏的导航站&#xff08;点击链接&#xff0c;跳转到专栏主页&#xff0c;欢迎订阅&#xff0c;持续更新…&#xff09; 专栏介绍&#xff1a;通过源码实例来讲解Asp.Net Core进阶知识点&#xff0c;让大家完全掌握每一…

分布式限流——Redis + Lua脚本实现令牌桶算法

主要思路概括如下&#xff1a; 定义数据结构&#xff1a; 使用Redis存储令牌桶的状态&#xff0c;包括当前令牌数&#xff08;KEYS[1]&#xff09;和上一次令牌填充的时间戳&#xff08;KEYS[1]:last&#xff09;。 计算新增令牌&#xff1a; 获取当前系统时间与上次令牌填充时…

中科亿海微-CL1656功能验证开发板

I. 引言 A. 研究背景与意义 CL1656是一款精度高、功耗低、成本低的5V单片低功耗运放&#xff0c;由核心互联公司研发制造&#xff0c;CL1656 是一个 16-bit、快速、低功耗逐次逼近型 ADC&#xff0c;吞吐速率高达 250 kSPS&#xff0c;并且内置低噪声、宽 带宽采样保持放大器。…

Vision Pro 零基础教程:1.机器视觉概述

文章目录 机器视觉简介机器视觉的发展历史机器视觉的结构组成机器视觉的应用工业相机分类1. 按传感器类型分类&#xff1a;2. 按分辨率分类&#xff1a;3. 按扫描方式分类&#xff1a;4. 按输出信号类型分类&#xff1a;5. 按应用领域分类&#xff1a;6. 按接口类型分类&#x…

React【Day2】

React表单控制 受控绑定 概念&#xff1a;使用React组件的状态&#xff08;useState&#xff09;控制表单的状态 双向绑定 MVVM 报错记录&#xff1a; 错误代码&#xff1a; import { useState } from "react";const App () > {const [value, setValue] useS…

使用pytorch构建GAN模型的评估

本文为此系列的第六篇对GAN的评估&#xff0c;上一篇为Controllable GAN。文中使用训练好的分类模型的部分网络提取特征将真实分布与生成分布进行对比来评估模型的好坏&#xff0c;若有不懂的无监督知识点可以看本系列第一篇。 原理 一般来说&#xff0c;我们评估模型的好坏可…

DataGridView添加行号隔行变色

运行效果 颜色对应关系 类实现代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms;namespace WindowsFormsApp1 {…

二刷大数据(二)- Spark

目录 SparkHadoop区别核心组件运行架构Master&WorkerApplication (Driver)Executor RDD概念yarn下工作原理算子依赖血缘关系阶段划分广播变量 shuffle流程SparkSQLDataSet、DataFrame、RDD相互转换 SparkStreaming Spark Spark是一种基于内存的快速、通用、可扩展的大数据…

C# Solidworks二次开发:比较两个solidworks文档属性相关API详解

大家好&#xff0c;今天要讲的文章是关于如何比较两个solidworks文档。 下面是API的介绍&#xff1a; &#xff08;1&#xff09;第一个为Close&#xff0c;这个API的含义为在比较solidworks文档以后执行必要的清理。下面是官方的具体解释&#xff1a; 其没有输入参数&#x…

MySQL Workbench下载安装、 MySQL Workbench使用

官方下载链接;MySQL :: Download MySQL Workbench 下载好懒人安装&#xff0c;也可自己选择目录 下面是使用&#xff1a; 连接数据库&#xff1a; 填写数据库连接信息&#xff1a; 基本操作部分&#xff1a; 数据导入导出&#xff1a; 导出/备份 导入&#xff1a; 生产er图…

【机器学习】科学库使用第5篇:Matplotlib,学习目标【附代码文档】

机器学习&#xff08;科学计算库&#xff09;完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;机器学习&#xff08;常用科学计算库的使用&#xff09;基础定位、目标&#xff0c;机器学习概述定位,目标,学习目标,学习目标,1 人工智能应用场景,2 人工智能小…

react中关于类式组件和函数组件对props、state、ref的使用

文章中有很多蓝色字体为扩展链接&#xff0c;可以补充查看。 常用命令使用规则 组件编写方式: 1.函数式 function MyButton() { //直接return 标签体return (<>……</>); }2.类 class MyButton extends React.Component { //在render方法中&#xff0c;return…

UE5 C++ 射线检测

一.声明四个变量 FVector StartLocation;FVector ForwardVector;FVector EndLocation;FHitResult HitResult;二.起点从摄像机&#xff0c;重点为摄像机前9999m。射线检测 使用LineTraceSingleByChannel 射线直线通道检测&#xff0c;所以 void AMyCharacter::Tick(float Delt…

GPT国内能用吗

2022年11月&#xff0c;Open AI发布ChatGPT&#xff0c;ChatGPT展现了大型语模型在自然语言处理方面的惊人进步&#xff0c;其生成文本的流畅度和连贯性令人印象深刻&#xff0c;为AI应用打开了新的可能性。 ChatGPT的出现推动了AI技术在各个领域的应用&#xff0c;例如&#x…

Python学习教程(Python学习路线+Python学习视频):Python数据结构

数据结构引言&#xff1a; 数据结构是组织数据的方式&#xff0c;以便能够更好的存储和获取数据。数据结构定义数据之间的关系和对这些数据的操作方式。数据结构屏蔽了数据存储和操作的细节&#xff0c;让程序员能更好的处理业务逻辑&#xff0c;同时拥有快速的数据存储和获取方…

.net9 AOT编绎生成标准DLL,输出API函数教程-中国首创

1&#xff0c;安装VS2022预览版&#xff08;Visual Studio Preview&#xff09; https://visualstudio.microsoft.com/zh-hans/vs/preview/#download-preview 2&#xff0c;选择安装组件&#xff1a;使用C的桌面开发 和 .NET桌面开发 ------------------------------------- …

java八股文知识点讲解(个人认为讲的比较好的)

1、解决哈希冲突——链地址法&#xff1a;【第7章查找】19哈希表的查找_链地址法解决哈希冲突_哔哩哔哩_bilibili 2、解决哈希冲突——开放地址法 &#xff1a; 【第7章查找】18哈希表的查找_开放定址法解决哈希冲突_哔哩哔哩_bilibili 3、小根堆大根堆的创建&#xff1a;选择…

【每日刷题】Day17

【每日刷题】Day17 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 19. 删除链表的倒数第 N 个结点 - 力扣&#xff08;LeetCode&#xff09; 2. 162. 寻找峰值 - 力扣…

1 回归:锂电池温度预测top2 代码部分(一) Tabnet

2024 iFLYTEK A.I.开发者大赛-讯飞开放平台 TabNet&#xff1a; 模型也是我在这个比赛一个意外收获&#xff0c;这个模型在比赛之中可用。但是需要GPU资源&#xff0c;否则运行真的是太慢了。后面针对这个模型我会写出如何使用的方法策略。 比赛结束后有与其他两位选手聊天&am…