Golang事件:插件架构的事件发射器/调度器

huangapple go评论89阅读模式
英文:

Golang events: EventEmitter / dispatcher for plugin architecture

问题

在Node.js中,我可以使用EventEmitter轻松地创建一个类似WordPress的克隆版本,通过复制和构建一个钩子系统到CMS核心中,然后插件可以连接到这些钩子上。

现在,我需要在使用Go编写和移植的CMS中实现同样级别的可扩展性和核心隔离。基本上,我已经完成了核心部分,但为了使其真正灵活,我必须能够插入事件(钩子),并且插件可以连接到这些钩子上以提供额外的功能。

我不关心重新编译(动态/静态链接),只要不必修改核心来加载插件- CMS核心不应该被修改(就像WP、Drupal等)。

我注意到有一些相当不知名的项目试图在Go中实现类似于Node.js中的EventEmitter的事件机制:

https://github.com/CHH/eventemitter

https://github.com/chuckpreslar/emission

由于上述这两个项目并没有获得太多的流行度和关注,我不禁怀疑这种关于事件的思考方式可能不适用于Go?这是否意味着Go可能不适合这个任务?无法通过插件实现真正可扩展的应用程序?

似乎Go的核心中没有内置事件,而RPC似乎不是将插件集成到核心应用程序中的有效解决方案,就像它们是本地内置的一样,就像它们是主应用程序的一部分一样。

对于无缝插件集成到核心应用程序的最佳方法是什么?如何实现无限扩展点(在核心中)而无需每次都修改核心来连接新插件?

英文:

In Node.js I was able to make a WordPress clone rather easily using the EventEmitter to replicate and build a hooks-system into the CMS core, which plugins could then attach to.

I now need this same level of extensibility and core isolation for my CMS written in and ported to Go. Basically I have the core finished now, but in order to make it truly flexible I have to be able to insert events (hooks) and to have plugins attach to these hooks with additional functionality.

I don't care about recompiling (dynamic / static linking), as long as you don't have to modify the core to load plugins - the CMS core should never be modified. (like WP, Drupal etc.)

I noticed there's a few rather unknown projects, trying to implement events in Go looking somewhat similar to EventEmitter in Node.js:

https://github.com/CHH/eventemitter

https://github.com/chuckpreslar/emission

Since those 2 projects above haven't gained much popularity and attention somehow I feel this way of thinking about events might now be how we should do it in Go? Does this mean Go is maybe not geared to this task? To make truly extendable applications through plugins?

Doesn't seem like Go has events built into its core, and RPC doesn't seem like a valid solution for integrating plugins into your core application as were they built in natively, and as if they were part of the main application itself.

What's the best way for seamless plugin integration into your core app, for unlimited extension points (in core) without manipulating core every time you need to hook up a new plugin?

答案1

得分: 36

一般来说,在Go语言中,如果你需要事件,你可能需要使用通道(channels),但如果你需要插件,最好使用接口(interfaces)。下面是一个简单的插件架构示例,它最大程度地减少了需要在应用程序的主文件中编写的代码来添加插件(这可以自动化,但不是动态的,请参见下文)。

希望这符合你的需求。


1. 插件接口

假设我们有两个插件,Fooer和Doer。我们首先定义它们的接口:

// 所有Doer插件在调用该方法时可以执行某些操作
type DoerPlugin interface {
    DoSomething()
}

// 所有Fooer插件在需要时可以执行Foo()方法
type FooerPlugin interface {
    Foo()
}

2. 插件注册表

现在,我们的核心应用程序有一个插件注册表。这里我只是简单地做了一些快速而简单的事情,只是为了让你理解这个想法:

package plugin_registry

// 这些是已注册的Fooer插件
var Fooers = []FooerPlugin{}

// 这些是已注册的Doer插件
var Doers = []DoerPlugin{}

现在我们公开了向注册表添加插件的方法。简单的方法是每种类型添加一个方法,但你也可以使用更复杂的反射方法,只需一个函数即可。但通常在Go语言中,尽量保持简单 Golang事件:插件架构的事件发射器/调度器

package plugin_registry

// 注册一个Fooer插件
func RegisterFooer(f FooerPlugin) {
    Fooers = append(Fooers, f)
}

// 注册一个Doer插件
func RegisterDoer(d DoerPlugin) {
    Doers = append(Doers, d)
}

3. 实现和注册插件

现在,假设这是你的插件模块。我们创建一个插件,它是一个Doer,在我们包的init()方法中注册它。init()方法在程序启动时对每个导入的包只执行一次。

package myplugin

import (
    "fmt"
    "github.com/myframework/plugin_registry"
)

type MyPlugin struct {
    // 任何内容
}

func (m *MyPlugin) DoSomething() {
    fmt.Println("Doing something!")
}

// 这里是自动注册包的"init魔法"
func init() {
    my := &MyPlugin{}
    plugin_registry.RegisterDoer(my)
}

4. 导入插件会自动注册它们

现在,我们唯一需要更改的是导入到我们的主包中的内容。由于Go语言没有动态导入或链接,这是你唯一需要编写的内容。通过查看文件树或配置文件并找到所有需要导入的插件,创建一个go generate脚本来生成一个主文件非常简单。它不是动态的,但可以自动化。因为主包导入插件是为了注册的副作用,所以导入时使用空白标识符以避免未使用的导入错误。

package main

import (
    "github.com/myframework/plugin_registry"
    _ "github.com/d00dzzzzz/myplugin" // 导入这个将自动注册插件
)

5. 在应用程序的核心部分

现在,我们的核心应用程序不需要任何代码更改就能与插件进行交互:

func main() {
    for _, d := range plugin_registry.Doers {
        d.DoSomething()
    }

    for _, f := range plugin_registry.Fooers {
        f.Foo()
    }
}

就是这样。请记住,插件注册表应该是一个独立的包,核心应用程序和插件都可以导入它,这样就不会出现循环导入的问题。

当然,你可以将事件处理程序添加到这个混合中,但正如我所演示的,这并不是必需的。

英文:

In general, in Go, if you need events you probably need to use channels, but if you need plugins, the way to go is
interfaces. Here's a bit lengthy example of a simple plugin architecture that minimizes the code that needs to be
written in the app's main file to add plugins (this can be automated but not dnyamic, see below).

I hope it's in the direction you're looking for.


1. The Plugin Interfaces

So okay, let's say we have two plugins, Fooer and Doer. We first define their interfaces:

// All DoerPlugins can do something when you call that method
type DoerPlugin interface {
	DoSomething() 
}

// All FooerPlugins can Foo() when you want them too
type FooerPlugin interface {
	Foo()
}

2. The Plugin Registry

Now, our core app has a plugin registry. I'm doing something quicky and dirty here, just to get the idea across:

package plugin_registry

// These are are registered fooers
var Fooers = []FooerPlugin{}

// Thes are our registered doers
var Doers = []DoerPlugin{}

Now we expose methods to add plugins to the registry. The simple way is to add one per type, but you could
go with more complex reflection stuff and have one function. But usually in Go, try to keep things simple Golang事件:插件架构的事件发射器/调度器

package plugin_registry

// Register a FooerPlugin
func  RegisterFooer(f FooerPlugin) {
	Fooers = append(Fooers, f)
}

// Register a DoerPlugin
func RegisterDoer(d DoerPlugin) {
	Doers = append(Doers, d)
}

3. Implementing and registering a plugin

Now, suppose this is your plugin module. We create a plugin that is a doer, and in our package's
init() method we register it. init() happens once on program started for every imported package.

package myplugin 

import (
	"github.com/myframework/plugin_registry"
)
type MyPlugin struct {
	//whatever
}

func (m *MyPlugin)DoSomething() {
	fmt.Println("Doing something!")
}

Again, here is the "init magic" that registers the package automatically

func init() {
	my := &MyPlugin{}
	plugin_registry.RegisterDoer(my)
}

4. Importing the plugins registers them automatically

And now, the only thing we'll need to change is what we import into our main package. Since
Go doesn't have dynamic imports or linking, this is the only thing you'll need to write.
It's pretty trivial to create a go generate script that will generate a main file
by looking into the file tree or a config file and finding all the plugins you need to import.
It's not dynamic but it can be automated. Because main imports the plugin for the side effect of the registration, the import uses the blank identifier to avoid unused import error.

package main

import (
	"github.com/myframework/plugin_registry"
	
	_ "github.com/d00dzzzzz/myplugin" //importing this will automaticall register the plugin
)

5. In the app's core

And now our core app doesn't need any code to change to be able to interact with plugins:

func main() {
	
	
	for _, d := range plugin_registry.Doers {
		d.DoSomething()
	}
	
	for _, f := range plugin_registry.Fooers {
		f.Foo()
	}
	
}

And that's about it. Keep in mind that the plugin registry should be a separate package
that both the app's core and the plugins can import, so you won't have circular imports.

Of course you can add event handlers to this mix, but as I've demonstrated, it's not needed.

huangapple
  • 本文由 发表于 2015年1月18日 00:55:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/28001872.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定