The code in this repository shows how to use the new plugin
package in Go 1.8 (see https://tip.golang.org/pkg/plugin/). A Go plugin is package compiled with the -buildmode=plugin
which creates a shared object (.so
) library file instead of the standar archive (.a
) library file. As you will see here, using the standar library's plugin
package, Go can dynamically load the shared object file at runtime to access exported elements such as functions an variables.
You can read the related article on Medium.
The plugin system requires Go version 1.8. At this time, it is only supports plugin on Linux. Attempt
to compile plugins on OSX, for instance, will result in -buildmode=plugin not supported on darwin/amd64
error.
The demo in this repository implements a simple greeting system. Each plugin package (directories ./eng
and ./chi
) implements code that prints a greeting meesage in a different lanaguage. File ./greeter.go
uses the new Go plugin
package to load the pluggable modules and displays the proper message using passed command-line parameters.
For instance, when the program is executed it prints a greeting in English or Chinese using the passed parameter to select the plugin to load for the appropriate language.
> go run greeter.go english
Hello Universe
Or to do it in Chinese:
> go run greeter.go chinese
你好宇宙
As you can see, the capability of the driver program is dynamically expanded by the plugins allowing it to display a greeting message in different language without the need to recompile the program.
Let us see how this is done.
To create a pluggable package is simple. Simply create a regular Go package designated as main
. Use the capitalization rule to indicate functions and variables that are exported as part of the plugin. This is shown below in file ./eng/greeter.go
. This plugin is responsible for displaying a message in English
.
File ./eng/greeter.go
package main
import "fmt"
type greeting string
func (g greeting) Greet() {
fmt.Println("Hello Universe")
}
// this is exported
var Greeter greeting
Notice a few things about the pluggable module:
- Pluggable packages are basically regular Go packages
- The package must be marked
main
- The exported variables and functions can be of any type (I found no documented restrictions)
The previous code exports variable Greeter
of type greeting
. As we will see later, the code that will consume this exported value must have a compatible type for assertion. One way this can be handled is to have an interface with the same method set. (The plugin package in directory ./chi
is exactly the same code except the message is in Chinese.)
The plugin package is compiled using the normal Go toolchain. The only requirement is to use the buildmode=plugin
compilation flag as shown below:
go build -buildmode=plugin -o eng/eng.so eng/greeter.go
go build -buildmode=plugin -o chi/chi.so chi/greeter.go
The compilation step will create ./eng/eng.so
and ./chi/chi.so
plugin files respectively.
Once the plugin modules are available, they can be loaded dynamically using the Go standard library's plugin
package. Let us examine file ./greeter.go, the driver program that loads and uses the plugin at runtime. Loading and using a shared object library is done in several steps as outlined below:
import (
...
"plugin"
)
The exported elements, from the pluggable package, can be of any type. The consumer code, loading the plugin, must have a compatible type defined (or pre-defined in case of built-in types) for assertion. In this example we define interface type Greeter
as a type that will be asserted against the exported variable from the plugin module.
type Greeter interface {
Greet()
}
The .so
file must be in a location accessible from you program in order to open it. In this example, the file .so files are located in directories ./eng
and ./chi
. and are selected based on the value of a command-line argument. The selected name is then assigned to variable mod
.
func main() {
// determine module to load
lang := "english"
if len(os.Args) == 2 {
lang = os.Args[1]
}
var mod string
switch lang {
case "english":
mod = "./eng/eng.so"
case "chinese":
mod = "./chi/chi.so"
default:
fmt.Println("don't speak that language")
os.Exit(1)
}
...
Using the Go standard library's plugin
package, we can now open
the plugin module. That step creates a value of type *plugin.Plugin
. It is used later to manage access to the plugin's exported elements.
func main(){
...
// load module
// 1. open the so file to load the symbols
plug, err := plugin.Open(mod)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
...
Next, we use the *plugin.Plugin
value to search for symbols that matches the name of the exported elements from the plugin module. In our example plugin (./eng/greeter.go, seen earlier), we exported a variable called Greeter
. Therefore, we use plug.Lookup("Greeter")
to locate that symbol. The loaded symbol is then assigned to variable symGreeter
(of type package.Symbol
).
func main(){
...
// 2. look up a symbol (an exported function or variable)
// in this case, variable Greeter
symGreeter, err := plug.Lookup("Greeter")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
...
Once we have the symbol loaded, we still have one additional step before we can use it. We must use type assertion to validate that the symbol is of an expected type and (optionally) assign its value to a variable of that type. In this example, we assert symbol symGreeter
to be of interface type Greeter
with symGreeter.(Greeter)
. Since the exported symbol from the plugin module ./eng/eng.so
is a variable with method Greet
attached, the assertion is true and the value is assigned to variable greeter
. Lastly, we invoke the method from the plugin module with greeter.Greet()
.
func main(){
...
// 3. Assert that loaded symbol is of a desired type
// in this case interface type Greeter (defined above)
var greeter Greeter
greeter, ok := symGreeter.(Greeter)
if !ok {
fmt.Println("unexpected type from module symbol")
os.Exit(1)
}
// 4. use the module
greeter.Greet()
}