Initialise & Service Dependencies

The second lifecycle stage a service goes through is init. Here a service can declare any other services it has a dependency on or any command line flags it requires.

Since v1.1.0 the Init lifecycle has been replaced by Injection for most purposes.

There are some use-cases where Init is still useful but for most purposes you will find Injection is neater and easier to maintain with less boilerplate code.

To do this the service needs to implement the InitialisableService interface:

1type InitialisableService interface {
2  Init(*Kernel) error
3}

If an error is returned from this Init() method the Kernel will exit immediately returning that error.

A service must never call a function in another service from inside the Init() method, nor create any external resources like go routines or open files.

Doing so could call a service which has not yet been initialised and leave resources open if the kernel exits due to an error.

Command line flags

To add a simple flag to a service we simply use the flag package to create the flag from within the Init function.

 1package hugo
 2
 3import (
 4    "flag"
 5    "github.com/peter-mount/documentation/tools/util"
 6    "github.com/peter-mount/go-kernel"
 7)
 8
 9// Hugo runs hugo
10type Hugo struct {
11    server *bool // true to run Hugo in server mode
12}
13
14func (h *Hugo) Name() string {
15    return "hugo"
16}
17
18func (h *Hugo) Init(_ *kernel.Kernel) error {
19    h.server = flag.Bool("s", false, "Run hugo in server mode")
20    return nil
21}

Once the kernel has completed the Init stage we can then reference the flag's value as it would be set with the command line flag from the command line.

Service dependencies

A service can declare a dependency against another service by using the Kernel instance passed to the Init method. The passed instance is only valid for this call, and you should never store it or attempt to use it outside of the Init lifecycle stage.

There are two types of dependencies. The most common one is where you want to use one service from another. To do this you need to use the AddService() function in the Kernel instance passed to Init().

func (k *Kernel) AddService(s Service) (Service, error)

This function accepts a single instance of the service you require. The function will then either return the instance the service that's been deployed in the kernel which you can then cast and store for later use.

If an error occurs then AddService() will return that error. You must exit the Init() function immediately, returning that error.

For example:

 1package hugo
 2
 3import (
 4    "context"
 5    "github.com/peter-mount/go-kernel"
 6)
 7
 8type Webserver struct {
 9    config *Config // Config
10}
11
12func (w *Webserver) Name() string {
13    return "webserver"
14}
15
16func (w *Webserver) Init(k *kernel.Kernel) error {
17    service, err := k.AddService(&Config{})
18    if err != nil {
19        return err
20    }
21    w.config = service.(*Config)
22
23    return nil
24}

The instance you pass to AddService() may not be the same one returned if that service already exists in the kernel.

The key here is the string the Name() function returns. As that must be unique it is used as the unique identifier within the kernel for the service.

As such the lookup follows the following rules:

  • If it already exists then the existing entry will be returned.
  • If it does not exist then the kernel will perform the following in sequence:
    1. If the new Service implements InitialisableService then it's Init() function will be called so that it can add its own dependencies which will then deploy before it.
    2. The new service is finally added to the kernel and the instance you passed to the function will be returned.

If the kernel has a service with the same name defined but of a different type then the cast will cause a panic stopping the kernel.

Unreferenced Service dependencies

The Kernel instance has a second function available, DependsOn(). This is rarely used but is a convenience function where you declare that you depend on one or more services to exist but don't actually want a reference to them.

func (k *Kernel) DependsOn(services ...Service) error

For example, you might have a service that requires a webserver to be running, but you don't need to directly link to it as you would be making http calls to it instead.

 1package pdf
 2
 3import (
 4    "github.com/peter-mount/documentation/tools/hugo"
 5    "github.com/peter-mount/go-kernel"
 6)
 7
 8// PDF tool that handles the generation of PDF documentation of a "book"
 9type PDF struct {
10    config *hugo.Config // Config
11    chromium *hugo.Chromium // Chromium browser
12}
13
14func (p *PDF) Name() string {
15    return "PDF"
16}
17
18func (p *PDF) Init(k *kernel.Kernel) error {
19    service, err := k.AddService(&hugo.Config{})
20    if err != nil {
21        return err
22    }
23    p.config = service.(*hugo.Config)
24
25    service, err = k.AddService(&hugo.Chromium{})
26    if err != nil {
27        return err
28    }
29    p.chromium = service.(*hugo.Chromium)
30
31    // We need a webserver & must run after hugo
32    return k.DependsOn(&hugo.Webserver{}, &hugo.Hugo{})
33}

Here we depend on two services which we store a reference to use them, but we also require two others to be deployed and started before this service.

If an error occurs then DependsOn() will return that error. You must exit Init() returning that error.