The case for plugins in Go
Plugins are useful for extending an application’s feature list – but in Go, compiling a whole app from source is fast and easy, so why should anyone bother with plugins in Go?
- First, loading a plugin at runtime may be a requirement in the app’s technical specification.
- Second, fast compilation and plugins are no contradiction. Go plugins can be created to be compiled into the binary – we’ll look at an example of this later.
This article is a quick survey of plugin architectures and techniques in Go.
Here is a wishlist for the ideal plugin architecture:
- Speed: Calling a plugin’s methods must be fast. The slower the method call is, the more is the plugin restricted to implementing only big, long-running, rarely called methods.
- Reliability: Plugins should not fail or crash, and if they do, recovery must be possible, fast, easy, and complete.
- Security: Plugins should be secured against tampering, for example, through code signing.
- Ease of use: The plugin programmer should not be burdened with a complicated, error-prone plugin API.
The ideal plugin architecture should meet all of the above criteria, but in real life there is usually one or another concession to make. This becomes immediately clear when we look at the question that should be the first one when deciding upon a plugin architecture:
Shall the plugins run inside the main process, or rather be separate processes?
In-process vs separate processes
Both approaches have advantages and disadvantages, and as we’ll see, one approach’s disadvantage may be the other’s advantage.
Advantages of in-process plugins
- Speed: Method calls are as fast as can be.
- Reliability: The plugins are available as long as the main process is available. An in-process plugin cannot suddenly become unavailable at runtime.
- Easy deployment: The plugin gets deployed along with the binary, either baked right in, or (only in non-Go languages for now) as a dynamic shared library that can be loaded either at process start or during runtime.
- Easy runtime management: No need for discovering, starting, or stopping a plugin process. No need for health checks. (Does the plugin process still live? Does it hang? Does it need a restart?)
Advantages of plugins as separate processes
- Resilience: A crashing plugin does not crash main process.
- Security: A plugin in a separate process cannot mess with internals of the main process.
- Flexibility (part 1): Plugins can be written in any language, as long as there is a library available for the plugin protocol.
- Flexibility (part 2): Plugins can be activated and deactivated during runtime. It is even possible to deploy and activate new plugins without having to restart the main process.
With these feature lists in mind, let’s look at a couple of different plugin solutions for the Go language.
Plugin approaches in Go
As mentioned before, Go lacks an option for loading shared libraries at runtime, and so a variety of alternate approaches have been created. Here are the ones I could find through two quick searches on GitHub and on Google, in no particular order:
External process using RPC via stdin/stdout
This is perhaps the most straightforward approach:
- Main process starts plugin process
- Main process and plugin process are connected via stdin and stdout
- Main process uses RPC (Remote Procedure Call) via stdin/stdout connection
The blog post Go Plugins are as Easy as Pie introduced this concept to Go in May 2015. The accompanying
pie package is here, and if you ask me, this could be my favorite plugin approach just for the yummy pumpkin pie picture in the readme! (Spoiler picture below.)
And this is basically how Pie starts a plugin and communicates with it:
In Pie, a plugin can take one of two roles.
- As a Provider, it responds to requests from the main program.
- As a Consumer, it can actively call into the main program and receive the results.
External process using RPC via network
The main difference to the previous approach is the way how the RPC calls are implemented. Rather than using the stdin/stdout connection, the RPC calls can also be done via the (local) network.
go-plugin by HashiCorp utilizes
net/rpc for connecting to the plugin processes.
go-plugin is a rather heavyweight plugin system with lots of features, clearly able to attract developers of enterprise software who look for a complete and industry tested solution.
External process via message queue
Message queue systems, especially the brokerless ones, provide a solid groundwork for creating plugin systems. My quick research did not return any MQ-based plugin solution, but this may well be due to the fact that not much is needed to turn a message queue system into a plugin architecture.
I did not find any message queue based plugin systems, but maybe you remember the first post of this blog, where I introduced the nanomsg system and its Go implementation Mangos. The nanomsg specification includes a set of predefined communication topologies (called “scalability protocols” in nanomsg jargon) covering many different scenarios: Pair, PubSub, Bus, Survey, Pipeline, and ReqRep. Two of them come in quite handy for communicating with plugins.
- The ReqRep (or Request-Reply) protocol can be used for mimicking RPC calls to a particular plugin. It is not the real RPC thing, however, as the sockets handle plain
bytedata only. So the main process and the plugins must take care of serializing and de-serializing the request and reply data.
- The Survey protocol helps monitoring the status of all plugins at once. The main process sends a survey to all plugins, and the plugins respond if they can. If a plugin does not respond within the deadline, the main process can take measures to restart the plugin.
In-process plugins, included at compile time
Calling a package a plugin might seem debatable when it is compiled into the main application just like any other package. But as long as there is a plugin API defined that the plugin packages implement, and as long as the build process is able to pick up any plugin that has been added, there is nothing wrong with that.
The advantages of in-process plugins–speed, reliability, ease of use–have been outlined above. As a downside, adding, removing, or updating a plugin requires compiling and deploying the whole main application.
Technically, any go library package can be a plugin package provided that it adheres to the plugin API that you have defined for your project.
Maybe the most common type of compile-time plugin is HTTP middleware. Go’s
net/http makes it super easy to plug in new handlers to an HTTP server:
- Write a package containing either one or more functions that implement the
Handlerinterface, or functions with the signature
func(w http.ResponseWriter, r *http.Request).
- Import the package into your application.
http.HandleFunc(<pattern>, <yourpkg.yourhandlefunc>), respectively, to register a handler.
Needless to say that this pattern can be used for any kind of plugin; the concept is not specific to HTTP handlers.
Script plugins: In-process but not compiled
Script plugin mechanisms provide an interesting middle ground between in-process and out-of-process plugin approaches. The plugin is written in a scripting language whose interpreter is compiled into your application. With this technique it is possible to load an in-process plugin at runtime–with the small caveat that the plugin is not native code but must be interpreted. Expect most of these approaches to have a rather low performance.
The page “Awesome-go.com” lists a couple of embeddable scripting languages for Go. Be aware that some of them include an interpreter while others only accept pre-compiled byte code.
Just to list a few here:
- Agora is a scripting language with Go-like syntax.
- GopherLua is an interpreter for the Lua scripting language.
The lack of shared, run-time loadable libraries did not stop the Go community from creating and using plugins. There are a number of different approaches to choose from, each one serving particular requirements.
Until Go supports creating and using shared libraries (and rumors about this appear to have been around since Go 1.4
A simple(-minded) plugin concept
All of the examples listed above have very good documentation and/or examples available. I refrain from repeating any code here and take a bare-bone approach instead, based upon
net/rpc (and a bit of
If you are not familiar with RPC in Go, you will want to keep the documentation of the
net/rpc package at hand while reading through the code.