Story time 📖
A few weeks back I was tasked with writing config driven black box monitoring tool. The requirements included two different handlers each with their own settings and no overlap. Now, in Python I might reach for dispatch pattern where I select the class of handler based on the Kind
specified in my configuration file. This got me thinking, how can I accomplish the same thing in Go?
Well, the go to package for inspecting types in the Go stdlib is reflect, but a glance at the docs shows me no way of inspecting defined types just their methods 🤦🏻♀️. After some more reading on the subject I settled on the stdlib plugin package.
So I worked up a demo of this tool with each of the handlers packaged as compiled plugins. It worked well, but it required some runtime helpers and my team lead pointed out that the register on init pattern used by the database/sql package could accomplish the dynamic type selection achieved by my current approach but without the helpers at runtime 🎉.
Let's dive into how this works 🤿
For starters, the whole pattern hinges on the ✨ specialness ✨ of the init()
function in Go. The init()
function is run on import of a package and regardless of how many times that package is imported, the init()
function will only be called once in a given package
block.
So, since Go doesn't offer a way for us to obtain listing of declared types, let's just create a mapping of our own and have each of our handler types add themselves to that mapping by declaring an init()
function in their respective package blocks.
Define an interface for our Component types and some helpers for mapping them
package components
import (
"fmt"
"log"
)
var (
Registry = make(map[string]Component)
)
type Component interface {
Greet() string
UnmarshalSettings([]byte) Component
}
// GetComponent returns the Component specified by name from `Registry`.
func GetComponent(kind string) (Component, error) {
// check if exists
if _, ok := Registry[kind]; ok {
return Registry[kind], nil
}
return nil, fmt.Errorf("%s is not a registered Component type", kind)
}
// Register is called by the `init` function of every `Component` to add
// the caller to the global `Registry` map. If the caller attempts to
// add a `Component` to the registry using the same name as a prior
// `Component` the call will log an error and exit.
func Register(kind string, c Component) {
// check for name collision before adding
if _, ok := Registry[kind]; ok {
log.Fatalf("Component: %s has already been added to the registry", kind)
}
Registry[kind] = c
}
Define our first Component and use an init() function to register it
package components
import (
"fmt"
"github.com/beautifulentropy/go-init-register-pattern/src/components"
"gopkg.in/yaml.v2"
)
type Component1 struct {
Input string `yaml:"input"`
}
func (c Component1) Greet() string {
return fmt.Sprintf(
"Hello, the input you provided to me was: %s", c.Input)
}
func (c Component1) UnmarshalSettings(settings []byte) components.Component {
var c1 Component1
yaml.Unmarshal(settings, &c1)
return c1
}
func init() {
components.Register("C1", Component1{})
}
Bring it all together in cmd/main.go
package main
import (
"log"
"github.com/beautifulentropy/go-init-register-pattern/src/components"
_ "github.com/beautifulentropy/go-init-register-pattern/src/components/component1"
_ "github.com/beautifulentropy/go-init-register-pattern/src/components/component2"
"gopkg.in/yaml.v2"
)
type config struct {
Kind string `yaml:"kind"`
Settings map[string]interface{} `yaml:"settings"`
}
func main() {
// Component1
// Our "fake" YAML input for Component 1 contains a key: `input`
// with a string value.
c1YAML := []byte(`
kind: C1
settings:
input: Over 9000
`)
// Unmarshal our YAML to a config struct so we can access `Kind` and
// use it's value to fetch the right Component type for `Settings`
// to get unmarshaled to.
var c1data config
yaml.Unmarshal(c1YAML, &c1data)
// Our component was registered as "C1" by the init method in
// `src/components/component1`. This was called when we performed
// our blank import on line 7 above. So we just have to fetch C1
// from the Registry
c1, err := components.GetComponent(c1data.Kind)
if err != nil {
log.Fatal(err)
}
// Marshal just the `Settings` field of `config` back to bytes and
// pass it to our `Component` type to be unmarshaled.
c1settings, _ := yaml.Marshal(c1data.Settings)
c1 = c1.UnmarshalSettings(c1settings)
log.Printf("type: '%T' says: %q", c1, c1.Greet())
What happens when we run this code?
- Each
init()
function is run and ourComponent
types are added to theRegistry
mapping. - We have some YAML that we load as a
config
object, so we can access theKind
field and request aComponent
ofconfig.Kind
. - We have our
Component
ofKind
, woot! - All of the keys in
config.Settings
should be a 1:1 mapping to fields in ourComponent
, so we can just useyaml.Unmarshal
to unpackconfig.Settings
into ourComponent
. - To accomplish this, we need
config.Settings
to be[]byte
which we can accomplish withyaml.Marshal
. - We pass our
Component
type'sUnmarshalSettings()
method YAML ofconfig.Settings
as[]byte
. - We get back a shiny
Component
ofconfig.Kind
and when we call theGreet()
method, we get exactly what we expect.
Let's run the code and see
2021/03/26 12:05:08 type: 'components.Component1' says: "Hello, the input you provided to me was: Over 9000"

Let's add another type and some YAML that makes use of it
package components
import (
"fmt"
"github.com/beautifulentropy/go-init-register-pattern/src/components"
"gopkg.in/yaml.v2"
)
type Component2 struct {
Input int `yaml:"input"`
}
func (c Component2) Greet() string {
return fmt.Sprintf("Yeah I'm the other one and the input you provided to me was: %d", c.Input)
}
func (c Component2) UnmarshalSettings(settings []byte) components.Component {
var c2 Component2
yaml.Unmarshal(settings, &c2)
return c2
}
func init() {
components.Register("C2", Component2{})
}
// Component2
c2YAML := []byte(`
kind: C2
settings:
input: 9001
`)
var c2data config
yaml.Unmarshal(c2YAML, &c2data)
c2, err := components.GetComponent(c2data.Kind)
if err != nil {
log.Fatal(err)
}
c2settings, _ := yaml.Marshal(c2data.Settings)
c2 = c2.UnmarshalSettings(c2settings)
log.Printf("type: '%T' says: %q", c2, c2.Greet())
Let's run our updated code
2021/03/26 12:05:08 type: 'components.Component1' says: "Hello, the input you provided to me was: Over 9000"
2021/03/26 12:05:08 type: 'components.Component2' says: "Yeah I'm the other one and the input you provided to me was: 9001"
What happens if we reference an unregistered Component type?
// Component3
c3YAML := []byte(`
kind: C3
settings:
input: Over 10000
`)
var c3data config
yaml.Unmarshal(c3YAML, &c3data)
// There is no C3 so this should fail.
c3, err := components.GetComponent(c3data.Kind)
if err != nil {
log.Fatalln(err)
}
c3settings, _ := yaml.Marshal(c3data.Settings)
c3 = c3.UnmarshalSettings(c3settings)
log.Printf("type: '%T' says: %q", c3, c3.Greet())
}
We should be greeted by the first two, but then fail on the third, let's see:
2021/03/26 12:05:08 type: 'components.Component1' says: "Hello, the input you provided to me was: Over 9000"
2021/03/26 12:05:08 type: 'components.Component2' says: "Yeah I'm the other one and the input you provided to me was: 9001"
2021/03/26 12:05:08 C3 is not a registered Component type
exit status 1
💯 🙆🏻♀️
Summary
The code for this blog post can be found at the link below.