Things to avoid while using Golang plugins

Alper Köse
Nov 12, 2018 · 4 min read

We are planning to open-source our project. There are some code about authorization logic that is highly specific to the company and we want to provide a way to not include that part of the code. But we still need to provide that same functionality. And while doing that we don’t want anyone to re-compile all the code just to make it work with their implementation of that specific part.

Our codebase is in Go which I love to work with. And while searching for possibilities, we came across golang-plugins. With golang plugins, you can import functions and variables from another file that was built via go build -buildmode=plugin command.

Gopher by Renée French (CC-BY 3.0)

We quickly got started and prepared a prototype implementation for our need. But, while working on that we came across some problems. Here are some of the problems we faced and ways to avoid them:

1) Different go versions

Both plugin implementation and the main application must be built with the exact same version of the Go toolchain. Depending on your GO version you will get an error like this:

panic: plugin.Open("simpleuser.plugin"): plugins must be built with the same version of the Go toolchain as the main application

OR

panic: plugin.Open(“simpleuser.plugin”): plugin was built with a different version of package github.com/alperkose/golangplugins/user

Workaround: None.

Since the code provided by plugin will run in the same process space with the main code, the compiled binary should be 100% compatible with the main application.

2) Different GOPATH’s

Both plugin implementation and the main application must be built with the exact same GOPATH. The error you will get is:

panic: plugin.Open(“differentgopath.plugin”): plugin was built with a different version of package github.com/alperkose/golangplugins/user

Workaround: use the same GOPATH (GOPATH in official docker image is /go) The issue https://github.com/golang/go/issues/19233 is opened for a similar situation.

Example code: https://github.com/alperkose/golangplugins

3) Using vendor folder

This seems a bit related to #2 but if you use vendor folder in either your plugin and/or your main application, you will get a very weird error:

panic: interface conversion: plugin.Symbol is func() user.Provider, not func() user.Provider

If you look closely, you will see that expectation func() user.Provider and the actual signature func() user.Provider are the same. This was a very confusing error but in all versions since 1.8 it exists.

Workaround: copy all your folders in your vendor folder to gopath while building your binary. This is a very dirty workaround and you need to do this for your plugin and your main application. If either one of these binaries are build with vendor folder, you cannot use the golang-plugin solution.

In our case, we were copying the folders in vendor and then we removed vendor folder during build phase. This was done in a docker image, so our local folder structure remained unchanged while during development.

...
RUN cp -r vendor/* $GOPATH/src && rm -rf vendor/
...

The issue https://github.com/golang/go/issues/18827 is opened for the same situation.

Example code: https://github.com/alperkose/golangplugins

4) Different versions of common dependencies

Any dependency you have in your plugin should be the same version with the dependencies in your main application.

Again, since the code provided by plugin will run in the same process space with the main code, the compiled binary should be 100% compatible with the main application. When you compile your binaries, the 3rd party packages are also compiled in your binary, but if there is a different version of the same function in your process space, your binary will panic since the compiled versions are not the same.

Workaround: use package managers and ensure the dependencies are the same version

You can find comments about this issue here: https://github.com/whiteboxio/flow/issues/3

5) Building static binaries

You cannot compile plugins into static binaries. I love static binaries since it removes the requirement of having a base image in your docker images. Using docker scratch image provides a minimal docker image and reduces one big dependency. When I tried to build a static binary out of plugin, I failed and you can find why in this article.

Workaround: None. You need to compile with CGO and if you’re using docker, you need a base image in your Dockerfile.


Conclusions

I think that golang-plugins are not a mature solution. They force your plugin implementation to be highly-coupled with your main application. The end-result is very brittle and hard to maintain even if you control the plugins and the main application. The overhead will be much higher if the author of the plugin does not have any control over the main application.

All of these issues pushed us to consider alternatives, in the end we’ve chosen to use hashicorp’s plugin package. It is based on RPC communication and provides us enough flexibility, however, it has it’s own limitations which are easier to overcome than golang-plugins

Code

You can find the code to test issues #2 and #3 here: https://github.com/alperkose/golangplugins

Alper Köse

Written by

ThoughtWorker @Germany

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade