godependency-injectiongo-uber-fx

Using uber fx to provide an interface


I am attempting to use uber fx to do dependency injection for a go microservice project.

Since all the microservices will need to construct a base server, and set up a variety of config options (common middleware, buffer sizes etc) (I am using fiber). But these different microservices also have config options unique to the microservice. Maybe a database connection string, jwt keys etc.

I created an interface to use in the shared function that creates the common base app, with the common options, but any function needing a dependency of the config struct fails with expecting the specific version of config for that microservice.

failed to build *fiber.App: missing dependencies for function "some-path/http".CreateServer (some-path/http/http.go:65): missing type: *http.Config exit status 1

Minimal example:

http/http.go

package http

import (
    "time"

    "github.com/gofiber/fiber/v2"
)


type BaseConfig interface {
    GetPort() string
    GetTimeout() int
}

type Config struct {
    Port           string `env:"LISTEN_ADDR" envDefault:":3000"`
    Timeout        uint64 `env:"TIMEOUT" envDefault:"10"`
}

func (c *Config) GetPort() string {
    return c.Port
}

func (c *Config) GetTimeout() int {
    return int(c.Timeout)
}

func CreateServer(config *Config) *fiber.App {
    fiberConfig := fiber.Config{
        ReadTimeout:    time.Second * time.Duration(config.GetTimeout()),
        WriteTimeout:   time.Second * time.Duration(config.GetTimeout()),
    }

    app := fiber.New(fiberConfig)

    // do setup and other stuff

    return app
}

some-service/config/config.go

package config

import (
    "github.com/caarlos0/env/v6"
    "github.com/rs/zerolog/log"
)

type Config struct {
    Port                string        `env:"LISTEN_ADDR" envDefault:":3000"`
    Timeout             uint64        `env:"TIMEOUT" envDefault:"10"`
    // some service specific stuff as well
}

func Parse() (*Config, error) {
    cfg := Config{}

    if err := env.Parse(&cfg); err != nil {
        return nil, err
    }

    return &cfg, nil
}

func (c *Config) GetPort() string {
    return c.Port
}

func (c *Config) GetTimeout() int {
    return int(c.Timeout)
}

some-service/main.go

package main

import (
    "context"
    "time"

    "some-path/http"
    "some-path/config"
    "some-path/controllers"
    "github.com/gofiber/fiber/v2"
    "go.uber.org/fx"
)

func main() {

    opts := []fx.Option{}
    opts = append(opts, provideOptions()...)
    opts = append(opts, fx.Invoke(run))

    app := fx.New(opts...)

    app.Run()
}

func provideOptions() []fx.Option {
    return []fx.Option{
        fx.Invoke(utils.ConfigureLogger),
        fx.Provide(config.Parse),
        fx.Invoke(controllers.SomeController),
    }
}

func run(app *fiber.App, config *config.Config, lc fx.Lifecycle) {
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            errChan := make(chan error)

            go func() {
                errChan <- app.Listen(config.Port)
            }()

            select {
            case err := <-errChan:
                return err
            case <-time.After(100 * time.Millisecond):
                return nil
            }
        },
        OnStop: func(ctx context.Context) error {
            return app.Shutdown()
        },
    })
}

some-path/controllers/some-controller.go

package controllers

import "some-path/config"

func SomeController (config *config.Config) {
    // do stuff
}

Solution

  • You're missing *http.Config object, create a function that return that object, e.g. NewConfig()

    package http
    
    import (
        "time"
    
        "github.com/caarlos0/env/v6"
        "github.com/gofiber/fiber/v2"
    )
    
    type BaseConfig interface {
        GetPort() string
        GetTimeout() int
    }
    
    type Config struct {
        Port    string `env:"LISTEN_ADDR" envDefault:":3000"`
        Timeout uint64 `env:"TIMEOUT" envDefault:"10"`
    }
    
    func NewConfig() (*Config, error) {
        cfg := Config{}
    
        if err := env.Parse(&cfg); err != nil {
            return nil, err
        }
    
        return &cfg, nil
    }
    
    func (c *Config) GetPort() string {
        return c.Port
    }
    
    func (c *Config) GetTimeout() int {
        return int(c.Timeout)
    }
    
    func CreateServer(config *Config) *fiber.App {
        fiberConfig := fiber.Config{
            ReadTimeout:  time.Second * time.Duration(config.GetTimeout()),
            WriteTimeout: time.Second * time.Duration(config.GetTimeout()),
        }
    
        app := fiber.New(fiberConfig)
    
        // do setup and other stuff
    
        return app
    }
    

    then change your provideOptions(), maybe like this:

    func provideOptions() []fx.Option {
        return []fx.Option{
            fx.Invoke(utils.ConfigureLogger),
            fx.Provide(config.Parse, http.NewConfig),
            fx.Invoke(controllers.SomeController),
            fx.Provide(http.CreateServer),
        }
    }