Categorygithub.com/protomesh/go-app
modulepackage
0.2.1
Repository: https://github.com/protomesh/go-app.git
Documentation: pkg.go.dev

# README

go-app

A small toolkit and microframework to create Golang applications, including features like:

  • Dynamic configuration loading through struct tags (including nested structs).
  • Configuration loading from multiple sources (command line flags, environment variables, configuration file).
  • Dependency injection pattern for nested structs.
  • Standard logging with uber-go/zap.

Creating an application

The code bellow is the minimal structure to create a new application with zero configurations and zero dependencies.

package main

import (
    "github.com/protomesh/go-app"
)

// This is the root of the dependency tree.
// All dependency are discovered from the struct attributes of
// this dependency annotated with the `config` tag.
struct *root {
    // Embed pointer to auto-instantiated injector.
    // This injector provides two methods for every embedding struct:
    // - `Dependency()` which returns the generic specified in the *app.Injector[D]
    // - `Log()` returning the app.Logger interface
    //
    // Its important to note that only the root dependency can have a struct pointer
    // like *root as its concretion. All child dependency must specify the D generic
    // for *app.Injector[D] as an interface which specifies the required dependency or `any`.
    *app.Injector[*root]

}

// Helper function
func newRoot() *root {
    return &root{}
}

// Must implement the app.Dependency interface
func (r *root) Dependency() *root {
    return r
}

var opts = &app.AppOptions{
    // Which flag set to assign, parse and read flags.
    // flag.CommandLine is the default instance set for command line applications.
    FlagSet:   flag.CommandLine,
    // Print configurations and dependency tree
    Print:     os.Getenv("PRINT_CONFIG") == "true",
}

func main() {
    
    deps := newRoot()

    myApp := app.NewApp(deps, opts)
    // Always defer the app close, it is required to flush any remaining log buffer.
    defer myApp.Close()

    // Your service startup logic here...

    // Blocks until the process receives an interruption signal
    app.WaitInterruption()

}

Dynamic configuration

To add dynamic configuration loading, you need to specify tags in the struct's attributes from the root of the dependency tree. The following code shows how to define a simple dependency in the dependency root:

struct *root {
    *app.Injector[*root]

    // Dynamic configuration must be a public attribute.
    MyString app.Config `config:"my.string,str" default:"mydefaultval" usage:"usage instructions"`
}

The config tag is used to specify the configuration key and type in the form of [config key],[type specifier]. All valid types are documented bellow:

Type specifierNameValid valuesDefault value
bool, booleanCase-insensitive booleant, true, y, yes, f, false, n, no, notfalse
int64, int, integer64-bit integer-0
float64, float, double64-bit floating point-0
durationDuration stringAny valid duration0
str, stringArbitrary length string-""
time, datetime, dateRFC3339 string-time.Time{}

The default lets you specify a default value if not provided by any configuration source. And the usage tag is used to display a help message for each configuration when the user calls your application with the -h (help) flag. Both tags, default and usage are optional.

Nested configuration

When you're abstracting pieces of your application you may want to keep the configuration needed for each component in the component itself. Lets say we have the following component of the application:

type MyNestedComponent struct {
    NestedVal app.Config `config:"val.number,int" default:"0" usage:"usage instructions"`
}

type MyComponent struct {
    ComponentVal app.Config `config:"val.text,str" default:"mydefaultval" usage:"usage instructions"`

    // In this case, we specify the child component only with the key parameter, the type specifier
    // default tag and usage instructions are not expected here.
    //
    // Note that this attribute must be public to be managebole by the go-app internals.
    //
    // The "nested" key is preffixed in the key with . as delimiter between the strings.
    MyNestedComponent *MyNestedComponent `config:"nested"`
}

To make the component configuration discoverable to the go-app framework, you just need to add it as a child configuration of your dependency root:

struct *root {
    *app.Injector[*root]

    // Dynamic configuration must be a public attribute.
    MyNestedComponent *MyNestedComponent `config:"component"`
}

In this case the following configuration are available:

  • component.nested.val.number
  • component.val.text

Dependency injection

The dependency tree injection feature is done with reflection. The first dependency is called root dependency, all other dependency are nested dependencies.

struct *root {
    *app.Injector[*root]

    DatabaseConnectionString app.Config `config:"database.connection,str" usage:"Database connection string"`

    db *sql.DB
}

func newRoot() *root {
    return &root{}
}

func (r *root) Dependency() *root {
    return r
}

func (r *root) GetDB() *sql.DB {
    return r.db
}


var opts = &app.AppOptions{
    FlagSet:   flag.CommandLine,
    Print:     os.Getenv("PRINT_CONFIG") == "true",
}

func main() {
    
    deps := newRoot()

    myApp := app.NewApp(deps, opts)
    defer myApp.Close()

    // Your service startup logic here...

    db, err := sql.Open("postgres", deps.DatabaseConnectionString.StringVal())
    if err != nil {
        myApp.Log().Panic("Error connecting")
    }

    myApp.db = db

    // Blocks until the process receives an interruption signal
    app.WaitInterruption()

}

Dependency tree

But as we mentioned in the configuration, you may want to specialize the components of your application abstracting responsibilities. Suppose that we have an PetStoreService that needs a *sql.DB pointer no matter where it came from (leaving the responsibility of injecting the dependency for the instantiating callee).


type PetStoreServiceDependencies interface {
    GetDB() *sql.DB
}

type PetStoreService[D PetStoreServiceDependencies] struct {
    *app.Injector[D]

    Name string `config:"name,str" default:"Animaland" usage:"The brand name of the pet store"`
}

func (p *PetStoreService[D]) DoAnything() {

    p.Log().Info("Do anything in the pet store", "logkey", "logval")

    db := p.Dependency().GetDB()

    // Do anything in the pet store...
}

struct *root {
    *app.Injector[*root]

    DatabaseConnectionString app.Config `config:"database.connection,str" usage:"Database connection string"`

    db *sql.DB

    // Its not mandatory to put the config tag, its only here for demonstration.
    PetStoreService *PetStoreService[*root] `config:"pet.store"`
}

And you can nest dependency, note that only the root dependency can specify a pointer to itself as the generic parameter for the nested dependencies. So if you have a nested dependency to the PetStoreService it would be implemented as the example bellow:


type PetStoreServiceDependencies interface {
    GetEmergencyChannel() <-chan string
}

type VeterinaryService[D PetStoreServiceDependencies] struct {
    *app.Injector[D]
}

func (v *VeterinaryService[D]) WatchForEmergencies() {

    emergencyCh := v.Dependency().GetEmergencyChannel()

    for message := range emergencyCh {
        v.Log().Warn("New emergency!!!", "message", message)
    }

}

var (
    // Ensure the correct implementation of methods for dependencies
    _ PetStoreServiceInjector = &PetStoreService[*root]{}
)

type PetStoreServiceInjector interface {
    GetEmergencyChannel() <-chan string
}

type PetStoreService[D PetStoreServiceDependencies] struct {
    *app.Injector[D]

    // Must be public
    VeterinaryService *VeterinaryService[PetStoreServiceInjector]
}

# Packages

No description provided by the author

# Functions

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Constants

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Variables

No description provided by the author

# Structs

No description provided by the author
No description provided by the author

# Interfaces

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Type aliases

No description provided by the author
No description provided by the author