I recently saw the plug-in loading mechanism of an internal project, which is very good. Of course, the plug-in mentioned here does not refer to the loading mechanism of golang native to the specified so file that can load the buildmode. It is a "plug-in" in software design. If your software is a framework or a platform product, if you want to improve scalability, you can allow third parties to develop third-party libraries and finally assemble these libraries like building blocks. Then this library loading mechanism may be required.
What are our goals? A certain library specification is implemented for third-party libraries. As long as it is developed according to this library specification, the library can be loaded into the framework.
Let's first define the data structure of a plug-in. Here we must use interfaces to standardize it. This can be freely used according to your project. For example, I hope the plug-in has a Setup method to load when it is started. Then I define the following Plugin structure.
type Plugin interface{ Name() string Setup(config map[string]string) error }
When the framework is started, I started a global variable like this:
var plugins map[string]Plugin
register
Some people may ask, there is a loading function setup here, but why is there no registration logic?
The answer is that the registration logic is placed in the init function of the library.
That is, the framework also provides a registration function.
// package plugin Register(plugin Plugin)
This register implements the placement of third-party plugins into the plugins global variable.
Therefore, the third-party plugin library is roughly implemented as follows:
package MyPlugin type MyPlugin struct{ } func (m *MyPlugin) Setup(config map[string]string) error { // TODO func (m *MyPlugin) Name() string { return "myPlugin" func init() { (&MyPlugin)
In this way, the registration logic becomes. If you want to load a plug-in, then you can directly introduce it in the form of _ import.
package main _ import "/foo/myplugin" func main() { }
Overall, the registration of plug-in is "hidden" into the import.
load
The registration logic actually looks ordinary, but the loading logic tests the details.
First of all, there are two points to consider when loading plugins:
- Configuration
- rely
Configuration means that the plug-in must have some configuration, which exists as paths in the configuration file yaml.
plugins: myplugin: foo: bar
Actually, I have reservations about this implementation. The configuration file exists in the form of a configuration item in a file, which seems to be worse than in the form of a configuration file, that is, in the file of config/plugins/.
This will not cause a large configuration file problem. After all, each configuration file itself is a DSL language. If you make the logic of the configuration file complicated, there will be many accompanying bugs caused by configuration file errors.
The second one is dependency. Plugin A depends on plugin B, so here is the order of loading function Setup. If this order of order is purely dependent on the user's "experience", it is very painful to place the Setup call of a certain plug-in before the Setup call of a certain plug-in. (Although there must be a way to do it). A better approach is to rely on the framework's own loading mechanism to load.
First, we define an interface in the plugin package:
type Depend interface{ DependOn() []string }
If my plugin relies on a plugin named "fooPlugin", then my plugin MyPlugin implements this interface.
package MyPlugin type MyPlugin struct{ } func (m *MyPlugin) Setup(config map[string]string) error { // TODO func (m *MyPlugin) Name() string { return "myPlugin" func init() { (&MyPlugin) func (m *MyPlugin) DependOn() []string { return []string{"fooPlugin"}
When we finally load all plugins, we do not simply call Setup by calling all plugins, but use a channel, put all plugins in the channel, and then call Setup one by one. When we encounter other plugins with Depend and the dependency plugins have not been loaded, we place the current plugin at the end of the queue (re-stuck into the channel).
var setupStatus map[string]bool // Get all registration pluginsfunc loadPlugins() (plugin chan Plugin, setupStatus map[string]bool) { // Here is a queue of length 10 var sortPlugin = make(chan Plugin, 10) var setupStatus = make[string]bool // All plugins for name, plugin := range plugins { sortPlugin <- plugin setupStatus[name] = false } return sortPlugin, setupStatus } // Load all pluginsfunc SetupPlugins(pluginChan chan Plugin, setupStatus map[string]bool) error { num := len(pluginChan) for num > 0 { plugin <- pluginChan canSetup := true if deps, ok := p.(Depend); ok { depends := () for _, dependName := range depends{ if _, setuped := setupStatus[dependName]; !setup { // There are unloaded plugins canSetup = false break } } } // If this plugin can be set up if canSetup { (xxx) setupStatus[()] = true } else { // If the plugin cannot be setup, the plugin will be stuffed into the last queue pluginChan <- plugin return nil }
The most exquisite thing about the above code is that it uses a buffer channel as a queue. SetupPlugins, the consumption queue side, is setupPlugins. In addition to the consumption queue, it is possible to produce data to the queue, which ensures that all plugins in the queue are loaded in order according to the tagged dependencies.
Summarize
The registration and loading mechanism of this plugin is very elegant. In terms of registration, cleverly use implicit import to register plug-in. In terms of loading, the buffer channel is cleverly used as the loading queue.
This is the article about the registration and loading mechanism of Golang library plug-in. For more related content of Golang plug-in mechanism, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!