Among the various fantastic and awesome features that the Go language provides, it also allows dynamic loading of code with Go Plugins. Go added this functionality in v1.8 and community has been super excited to see this welcome change.
So why the need to build plugins? How do they work? In this blog, we’ll learn why plugins are so important, how to create them, build, load and use plugins in Go programs.
Why Plugins?
Go plugins enable developers to build loosely coupled modular programs. Go packages can be compiled as shared object (.so) libraries and can be loaded dynamically at run time.
With Go Plugins –
- Developers can achieve code modularization which is imperative in large systems
- Break down the code into a reusable library that adhere to strict interface
- Test the reusable code so that it can be used to serve one clear purpose
- Ensures use of best practices: loosely coupled systems and separation of concerns
Cool, lets go ahead and create a plugin.
Creating Go Plugins
A Go plugin looks very similar to any standard Go code, the difference comes only when we try to build the plugin. Simply put, a Go plugin is like any other Go package and becomes a plugin when we build it.
So let’s build a math plugin that performs multiple mathematical operations. We start by creating a package main that performs basic mathematical operations viz, addition and subtraction. Let’s create this file as math.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "fmt" | |
// Add two integers | |
func Add(a int, b int) int { | |
fmt.Printf("\nAdding a=%d and b=%d", a, b) | |
return a + b | |
} | |
// Sub two integers | |
func Sub(a int, b int) int { | |
fmt.Printf("\nSubtracting b= %d from a=%d", b, a) | |
return a – b | |
} |
And that’s it the plugin is ready! Not really, we need to build the plugin and only then could we use the functions defined in the math.go file.
Building the Plugin
In the above section we created the package and exported the functions (remember all function names that start with capital alphabets are exported by default). Now, we have math plugin and we need to build to create a shareable library that can be loaded dynamically by our main program at run time.
We can build the Go plugin using the command:
go build -buildmode=plugin -o math.so
- Above command uses the buildmode=plugin that suggests the Go compiler to compile the code in plugin mode and not as a package or a command binary.
- math.so is the shared library or the plugin generated by Go compiler. You could choose to store the built plugin anywhere on your file system (generally recommended to have a plugins folder).
So we now have the math.so plugin that has exported functions and we’re ready to use them in our main program. Let’s create a file called main.go to load the plugin and use it.
Loading the Plugin
The only thing that you require to know before loading the plugin is it’s location. It is recommended that all plugins are built and stored in plugins folder. So this problem solves itself. But you could also choose to pass the plugin path as command line argument or environment variable based on how you see your program being used.
Following code snippet gets the plugin math.so and loads it.
We use the filepath.Glob() function from path/filepath package to get the plugin based on the location on file system. And the plugin package has Open() function that is leveraged to load the plugin.
// Glob - Gets the plugin to be loaded plugins, err := filepath.Glob("math.so") if err != nil { panic(err) } // Open - Loads the plugin fmt.Printf("Loading plugin %s", plugins[0]) p, err := plugin.Open(plugins[0]) if err != nil { panic(err) }
Checking the Plugin symbols
Loading the plugin was easy peasy and so is using it. But there are a few checks that our program should perform before using the exported functions. Let’s look at them.
// Lookup - Searches for a symbol name in the plugin symbol, err := p.Lookup("Add") if err != nil { panic(err) } // symbol - Checks the function signature addFunc, ok := symbol.(func(int, int) int) if !ok { panic("Plugin has no 'Add(int)int' function") }
In the above code snippet, we use the Lookup() method from the plugin object that given a symbol name returns an interface. So basically it returns the symbols from the shared library that can be used. If the symbol is not available, the program will thrown an error suggesting it couldn’t find the exported functions, aka symbols.
Once we have the symbol name, we assert the type of the symbol. So in this case we check if the symbol Add has the signature we expected it to have ie, func(int, int) int – function that takes two integer parameters and return integer value. If that works, the assert will return true and we can move forward using the function.
Note: there is no way for us to see all the exported symbols for a plugin. So you should know the symbol names and the signatures and assert on them before using them in your program.
Using the plugin
Finally we use the exported function from the plugin in the code below. We pass two parameters 3 and 4 and expect the sum to be printed. Neat! 🙂
// Uses the function to return results addition := addFunc(3, 4) fmt.Printf("\nAddition is:%d", addition)
The Complete Picture
Here’s the complete code snippet with comments for every stage:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"path/filepath" | |
"plugin" | |
) | |
func main() { | |
// Glob – Gets the plugin to be loaded | |
plugins, err := filepath.Glob("math.so") | |
if err != nil { | |
panic(err) | |
} | |
// Open – Loads the plugin | |
fmt.Printf("Loading plugin %s", plugins[0]) | |
p, err := plugin.Open(plugins[0]) | |
if err != nil { | |
panic(err) | |
} | |
// Lookup – Searches for a symbol name in the plugin | |
symbol, err := p.Lookup("Add") | |
if err != nil { | |
panic(err) | |
} | |
// symbol – Checks the function signature | |
addFunc, ok := symbol.(func(int, int) int) | |
if !ok { | |
panic("Plugin has no 'Add(int)int' function") | |
} | |
// Uses the function to return results | |
addition := addFunc(3, 4) | |
fmt.Printf("\nAddition is:%d", addition) | |
} |
Terminal output:
$ go run main.go Loading plugin math.so Adding a=3 and b=4 Addition is:7
Awesome! So we successfully created the plugin and used it in our Go program 🙂
I’m a Plugin developer – any advice for me?
Hope this article encourages you to write your own useful plugin and share it with the community. Here are a few tips that might help you:
- Plugin should be considered as independent as possible, loosely coupled and focused on only one functionality
- It should adhere to clearly defined contracts useful for plugin integration and easy to use
- Developers who import plugins at run time must consider plugins as black box and shouldn’t make any assumptions that are not defined in the contracts
- As the plugin is compiled, there’s an evident need for clear documentation
- Calling methods of a plugin should be real quick. Slower the plugin, it has high impact on the performance of the program
- Art of distributing plugins and versioning becomes critical as there’s no one right way to achieve this
If you do develop one, please share it with me, I will be happy to let everyone know about it! 🙂
Summarizing Go plugins
So we covered quite a few things about Plugins in Go. But here are the key takeaways I would want you to make a note of.
- Support for Go plugins started in v1.8 and in v1.11 it currently supports Linux and Mac operating systems (Sorry windows folks!)
- The “plugin” package provides a straight forward set of functions to dynamically load plugins as necessary that can help developers write extensible code. More about plugin package at https://golang.org/pkg/plugin/
- A Go plugin is a package compiled using the -buildmode=plugin build flag to produce a shared object (.so) library file
- The exported functions and variables, in the Go package, are exposed as ELF symbols that can be looked up and be bound to at run time using the plugin package
Good Reads
That’s it folks, hope enjoyed this blog, I highly recommend you to go through these links and as usual feel free to post feedback or suggestions.
- Golang import package: https://golang.org/pkg/plugin/
- Learning Go programming: https://medium.com/learning-the-go-programming-language/writing-modular-go-programs-with-plugins-ec46381ee1a9
- Applied Go: https://appliedgo.net/plugins/
Tada! 🙂