Writing modular software using Go plugins
The 1.8 version of Go bring with it the ability to writing modular software using plugins, how?
Each plugin is compiled as shared object library that can be loaded at runtime, read Vladimir Vivien’s article I learned how to write my own modular localization system using Go plugins!
Preface
Currently, plugins are only supported on Linux and MacOS and with the latest build of Go (1.11 currently) are been almost deprecated by Modules.
How build a plugin
A Go plugin is a package compiled with go build -buildmode=plugin
command, this will produce a shared object .so
file.
Exported functions and variables will be exposed as symbols in the share object file.
Writing a modular localization software
Get start with a simple project’s structure:
~/go/src/github.com/dlion/modularLocalization/$:> find .
./languages/english/english.go
./main.go
english.go
is our english language plugin
Writing a plugin
package main
func Speak() string {
return "Hello World"
}
We just defined a Speak
function that will be exported (pay attention to the uppercase naming convention) to return an english localized string.
Compiling with go build -buildmode=plugin -o languages/english/english.so languages/english/english.go
will produce and english.so
file inside our english
directory, that is our plugin.
~/go/src/github.com/dlion/modularLocalization/$:> find .
./languages/english/english.so
./languages/english/english.go
./main.go
How to use a plugin
Our main file:
package main
import "fmt"
import "os"
import "plugin"
func main() {
if len(os.Args) < 2 {
fmt.Printf("usage: ./%s <language plugin>", os.Args[0])
}
lang := os.Args[1]
symLanguage := lookupPlugin("./languages/"+lang+"/"+lang+".so", "Speak")
speak, ok := symLanguage.(func() string)
if !ok {
fmt.Println("The function signature is different")
os.Exit(1)
}
fmt.Printf("%s\n", speak())
}
func lookupPlugin(p, s string) plugin.Symbol {
plug, err := plugin.Open(p)
errorHandler(err)
sym, err := plug.Lookup(s)
errorHandler(err)
return sym
}
func errorHandler(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
It is composed by 2 parts, inside the lookupPlugin
function and another one just after calling it.
Open a plugin
plug, err := plugin.Open(p)
errorHandler(err)
The signature of the Open
method is func Open(path string) (*Plugin, error)
so we just need to pass as paramether the path where our plugin is.
Lookup Symbols
sym, err := plug.Lookup(s)
errorHandler(err)
The signature of the Lookup
function is func (p *Plugin) Lookup(symName string) (Symbol, error)
, it will search in the shared object file our symbol
to returns a Symbol
type that is just an interface{}
.
Use the Symbol
To use our symbol we need to ensure that the type is correct, so we need to cast to it:
speak, ok := symLanguage.(func() string)
if !ok {
fmt.Println("The function signature is different")
os.Exit(1)
}
if everything is fine we can call it like a normal function: fmt.Printf("%s\n", speak())
The output will be:
$:> go run main.go english
Hello World
Extend our software using another plugin
Now we want to extend the localization of our software using a chinese version, let’s see how!
~/go/src/github.com/dlion/modularLocalization/$:> find .
./languages/english/english.so
./languages/english/english.go
./languages/chinese/chinese.go
./main.go
and then our chinese plugin:
package main
func Speak() string {
return "你好,世界"
}
Compile it with go build -buildmode=plugin -o languages/chinese/chinese.so languages/chinese/chinese.go
And BAM!
$:> go run main.go chinese
你好,世界
$:> go run main.go english
Hello World
Without changing our main
file we have a plugins’ base localization system. Amazing, uh!?
Even at runtime
Using Go plugins we have the ability to load our plugins at runtime, so we don’t need to rebuild or kill our software, here an example:
package main
import "fmt"
import "os"
import "plugin"
func main() {
for {
var lang string
fmt.Print("Insert which language do you prefer: ")
fmt.Scanf("%s", &lang)
symLanguage := lookupPlugin("./languages/"+lang+"/"+lang+".so", "Speak")
speak, ok := symLanguage.(func() string)
if !ok {
fmt.Println("The function signature is different")
os.Exit(1)
}
fmt.Printf("%s\n", speak())
}
}
func lookupPlugin(p, s string) plugin.Symbol {
plug, err := plugin.Open(p)
errorHandler(err)
sym, err := plug.Lookup(s)
errorHandler(err)
return sym
}
func errorHandler(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
And the output will be:
~/go/src/github.com/dlion/modularLocalization/$:> go run main.go
Insert which language do you prefer: english
Hello World
Insert which language do you prefer: chinese
你好,世界
But Why?
Writing modular software using Go plugins allows to have a lot of benefits like:
- Single Responsability: Every plugin should do ONE thing.
- Plugin Independence: Every plugin is an independence piece of software
- Well-documented: Every plugin should be well-documented; to produce a working plugin you should respect the contract of the interface or the casting that we have on our main, so we need to know how it is.
- New Deployment paradigm: To extend our software we can just
push
our plugin on the right directory and DONE. We don’t need to recompile to extend the software functionalities.
Links
You can find the code here