Plugin fundamentals in Go
Plugin systems across languages tend to share four core concepts: how plugins are discovered, how they register with the host application, which hooks or mount points the application exposes, and how application capabilities are made available back to plugins. In Go, the way these concepts play out depends largely on whether the plugin is compiled into the binary or loaded at run time.
Compile-time plugins
Compile-time plugins are regular Go packages that are linked into the main executable. Once the binary is built, the set of plugins is fixed. The canonical example is the driver mechanism for database/sql.
A driver package is imported with a blank import (import _ "name") and its init function calls sql.Register to make itself available. The design maps cleanly onto the four plugin concepts:
- Discovery: explicit, via the import statement itself; registration happens automatically in
init. - Registration: straightforward—the plugin code invokes the registration function (
sql.Register) directly. - Application hooks: plugins implement an interface provided by the application (e.g.
driver.Driver) and register an instance of it. - Extension API: trivial, because the plugin is linked into the binary and can import utility packages from the main application.
Run-time plugins with shared libraries
For true run-time extensibility, Go provides the plugin package in the standard library. This lets you build Go code as shared libraries (with -buildmode=plugin) and load them at run time, retrieving symbols by name.
A complete worked example—an htmlize program that converts markup to HTML and accepts plugins to customize formatting—demonstrates the model in practice. The main application defines a PluginManager type that orchestrates everything:
LoadPluginsscans a directory for.sofiles.- Each plugin must expose a global
InitPluginfunction, which receives a*PluginManager. - Plugins register handlers (such as a
RegisterRoleHook) with the manager during initialization. - The application later consults the manager to dispatch work to the registered hooks.
func (pm *PluginManager) RegisterRoleHook(role string, hook RoleHook)
type RoleHook func(content *Recipe, db *content.DB, post *content.Post) (string, error)
RoleHook illustrates how application data flows back to plugins: the callback receives database access via content.DB and the specific post via content.Post. No Go interfaces are strictly required in this design; the application-to-plugin contract is just function types and data objects.
Compatibility constraints
The shared-library approach carries a notable drawback: Go is strict about version parity between the host binary and its plugins. If a package shared by both is modified and the application is rebuilt, loading an older plugin typically fails with an error like plugin: plugin was built with a different version of package ...
This rigidity is deliberate. In C/C++, using dlopen and dlsym returns untyped void* pointers; a signature mismatch after an update can cause crashes or memory corruption. Go's plugin package tries to prevent that by requiring identical package versions and even the same compiler version for the main program and its plugins.
The price is operational friction. Whenever a common dependency changes—even in ways that leave the plugin interface untouched—all plugins must be rebuilt along with the host. That complicates a workflow where plugins are often developed in separate repositories with independent release cycles.
RPC-based alternatives
Because the standard plugin package arrived relatively late (Go 1.8) and has the aforementioned versioning pain, the ecosystem produced an alternative: plugins as separate processes communicating over RPC. Decoupling plugins into their own binaries brings several benefits:
- Isolation: a plugin crash doesn't take down the main application.
- Language independence: with RPC as the boundary, the plugin needn't be Go.
- Distribution: network-enabled plugins can run on different machines.
Go's standard net/rpc package provides a solid base for this model. The prominent hashicorp/go-plugin library builds on it and additionally supports gRPC, whose built-in versioning helps solve the cross-version interoperability problem. This approach has seen significant production use in Hashicorp's own tooling.



