Plugins in Go
Go is a statically compiled language. The Go runtime cannot load dynamic libraries, nor does it support compiling Go on the fly. Still, there is a number of ways of creating and using plugins in Go.
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.
Plugin criteria
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
Description
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
Example
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
Description
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.
Example
The package
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
Description
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.
Example
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
[]byte
data 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
Description
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.
Example
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
Handler
interface, or functions with the signaturefunc(w http.ResponseWriter, r *http.Request)
. - Import the package into your application.
- Call
http.Handle(<pattern>, <yourpkg.yourhandler>)
orhttp.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
Description
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.
Example
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.
- Otto is a JavaScript interpreter.
Conclusion
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 os/exec
).
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.
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"os"
"os/exec"
"time"
)
The plugin
Plugin
is the type that we register with net/rpc
. The RPC client can
then call Plugin's methods.type Plugin struct {
listener net.Listener
}
Revert
returns the input string reverted.All methods callable via RPC must satisfy these conditions:
- exported method of exported type
- two arguments, both of exported or built-in type
- the second argument is a pointer
- one return value, of type error
func (p Plugin) Revert(arg string, ret *string) error {
fmt.Println("Plugin: revert")
l := len(arg)
r := make([]byte, l)
for i := 0; i < l; i++ {
r[i] = arg[l-1-i]
}
*ret = string(r)
return nil
}
Exit
terminates the plugin process.
arg
and ret
are only dummy parameters, to satisfy the
function signature.func (p Plugin) Exit(arg int, ret *int) error {
fmt.Println("Plugin: done.")
os.Exit(0) // using os.Exit here is suitable for demo code only.
return nil
}
start
starts the plugin server.func startPlugin() {
fmt.Println("Plugin start")
p := &Plugin{}
err := rpc.Register(p)
if err != nil {
log.Fatal("Cannot register plugin: ", err)
}
fmt.Println("Plugin: starting listener")
p.listener, err = net.Listen("tcp", "127.0.0.1:55555")
if err != nil {
log.Fatal("Cannot listen: ", err)
}
fmt.Println("Plugin: accepting requests")
rpc.Accept(p.listener)
}
The main application
app
is our main application. It starts the plugin process
and calls Shout()
via RPC.func app() {
fmt.Println("App start")
p := exec.Command("./plugins", "true")
p.Stdout = os.Stdout
p.Stderr = os.Stderr
err := p.Start()
if err != nil {
log.Fatal("Cannot start ", p.Path, ": ", err)
}
net/rpc
has no DialTimeout
method (unlike net
). time.Sleep(1 * time.Second)
fmt.Println("App: registering RPC client")
client, err := rpc.Dial("tcp", "127.0.0.1:55555")
if err != nil {
log.Fatal("Cannot create RPC client: ", err)
}
fmt.Println("App: calling Revert")
var reverse string
err = client.Call("Plugin.Revert", "Live on time, emit no evil", &reverse)
if err != nil {
log.Fatal("Error calling Revert: ", err)
}
fmt.Println("App: revert result:", reverse)
fmt.Println("App: stopping the plugin")
var n int
client.Call("Plugin.Exit", 0, &n)
p.Wait()
fmt.Println("App: done.")
}
main()
func main() {
if len(os.Args) > 1 && os.Args[1] == "true" {
startPlugin()
time.AfterFunc(10*time.Second, func() {
fmt.Println("Plugin: idle timeout - exiting")
return
})
} else {
app()
}
}
As always, get the code with go get -d
to avoid that the binary to show up in your $GOPATH/bin.
go get -d github.com/appliedgo/plugins
cd $GOPATH/src/github.com/appliedgo/plugins
go build
./plugins
Enjoy!
Imports