goextensibilityeventemitterplugin-architecture

Golang events: EventEmitter / dispatcher for plugin architecture


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?


Solution

  • 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 :)

    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.