10

I have an application which needs configuration and I’ve created a configuration struct and I’m entering the configuration as a parameter to the function. The problem is that the configuration struct becomes bigger (like monolith) and bigger and I move the config to different functions in my app and which doesn’t need all the fields, just few of them. My question is if there is better approach to implement it in Go.

After struggling to find good way I’ve found this article (which a bit old but hopefully still relevant) and I wonder how and if I can use it to solve my problem.

Functional options instead of config struct https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

I need to inject some configuration properties to my application in

For example for function run (which is entry point ) I need to inject the log level and some other env variable like port host

For function build I need to “inject” the build flavor and build type etc.

Any example for my content will be very helpful

  1. How to structure it in the code ?
  2. How to implement it?

update

I need some E2E example how can I use the functional approach for different configs in the same package and other packages

Jenny Hilton
  • 1,297
  • 7
  • 21
  • 40
  • Create a file containing separate struct for different functions and import it according to requirement in your main.go file – Himanshu Feb 27 '18 at 08:54
  • 1
    @Himanshu - yes for course I can do it but my question is if there is better approach ? – Jenny Hilton Feb 27 '18 at 08:56
  • Post some code that you have tried so far to get an idea. What and how you actually want – Himanshu Feb 27 '18 at 08:57
  • Each function will use a single struct or same struct can be used for different functions ? – Himanshu Feb 27 '18 at 09:02
  • @Himanshu - each struct can be used in different function , but its not mandatory . there is option that one struct can be used in one function... – Jenny Hilton Feb 27 '18 at 09:10
  • @Himanshu - are you familiar with this concept Functional options instead of config struct https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis – Jenny Hilton Feb 27 '18 at 09:11
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/165879/discussion-between-himanshu-and-jenny-hilton). – Himanshu Feb 27 '18 at 09:18

2 Answers2

4

It sounds like you're looking for an alternative to passing around the same configuration monolith structure to every package and every function. There are many solutions to this problem (more than I'm going to list here), and which one is right for you requires more knowledge of your code and your goals than we have, so it's probably best if you decide. And it sounds like you're wondering whether Dave Cheney's post on functional options provides a solution and how to apply it.

If your application's configuration is static in that it's not likely to change (mutate) through different threads of execution, and you don't need to create multiple instances with different configurations in the same main, then one option is package level variables and package initialization. If you object to exported package variables, you can use unexported package variables and control access via exported functions. Say run and build are two different packages:

// package main
import(
    "github.com/profilename/appname/build"
    "github.com/profilename/appname/run"
)
func main() {
    // do something to get configuration values
    build.Initialize(buildFlavor, buildType)
    // any post-build-initialize-pre-run-initialize stuff
    run.Initialize(logLevel, port, host)
    // other processing
    build.PreBuild("title") // other build functions may rely on configuration
    build.Build()
    // other stuff
    run.ReadFiles(f1, f2)
    run.Validate(preferredBackupPort) // port availability, chance to log.Fatal out
    run.Run()
    // cleanup
}

// package run
var Host string
var LogLevel, Port int
init() {
    Host = `localhost`
    Port = 8888
    Loglevel = 1
}
func Initialize(logLevel, port int, host string) {
    // validation, panic on failure
    LogLevel = logLevel
    Host = host
    Port = port
}
func Run() {
    // do something with LogLevel, Host, Port
}

But that doesn't solve the problem addressed in Dave Cheney's post. What if the user is running this without host, port, or buildType (or other configuration variables), because he doesn't need those features? What if the user wants to run multiple instances with different configurations?

Dave's approach is primarily intended for situations where you will not use package-level variables for configuration. Indeed, it is meant to enable several instances of a thing where each instance can have a different configuration. Your optional configuration parameters become a single variadic parameter where the type is a function that modifies a pointer to the thing being configured. For you, that could be

// package run
type Runner struct {
    Port        int
    // rest of runner configuration
}
func NewRunner(options ...func(*Runner)) (runner *Runner, err error) {
    // any setup
    for _, option := range options {
        err = option(runner)
        if err != nil {
            // do something
        }
    }
    return runner, err
}

// package main
func main() {
    // do something to get configuration values
    port := func(runner *Runner) {
        runner.Port = configuredPort
    }
    // other configuration if applicable
    runner := run.NewRunner(port)
    // ...

In a way, Dave's approach appears targeted at packages that will be used as very flexible libraries, and will provide application interfaces that users might wish to create several instances of. It allows for main definitions that launch multiple instances with different configurations. In that post he doesn't go into detail on how to process configuration input in the main or on a configuration package.

Note that the way the port is set in the resulting code above is not very different from this:

// package run
type Runner struct {
    Port        int
    // rest of runner configuration
}

// package main, func main()
    runner := new(run.Runner)
    runner.Port = configuredPort

which is more traditional, probably easier for most developers to read and understand, and a perfectly fine approach if it suits your needs. (And you could make runner.port unexported and add a func (r *Runner) SetPort(p int) { r.port = p } method if you wanted.) It is also a design that has the potential, depending on implementation, to deal with mutating configuration, multiple threads of execution (you'll need channels or the sync package to deal with mutation there), and multiple instances.

Where the function options design Dave proposed becomes much more powerful than that approach is when you have many more statements related to the setting of the option that you want to place in main rather than in run -- those will make up the function body.


UPDATE Here's a runnable example using Dave's functional options approach, in two files. Be sure to update the import path to match wherever you put the run package.

Package run:

package run

import(
    "fmt"
    "log"
)

const(
    DefaultPort = 8888
    DefaultHost = `localhost`
    DefaultLogLevel = 1
)

type Runner struct {
    Port        int
    Host        string
    LogLevel    int
}

func NewRunner(options ...func(*Runner) error) (runner *Runner) {
    // any setup

    // set defaults
    runner = &Runner{DefaultPort, DefaultHost, DefaultLogLevel}

    for _, option := range options {
        err := option(runner)
        if err != nil {
            log.Fatalf("Failed to set NewRunner option: %s\n", err)
        }
    }
    return runner
}

func (r *Runner) Run() {
    fmt.Println(r)
}

func (r *Runner) String() string {
    return fmt.Sprintf("Runner Configuration:\n%16s %22d\n%16s %22s\n%16s %22d",
        `Port`, r.Port, `Host`, r.Host, `LogLevel`, r.LogLevel)
}

Package main:

package main

import(
    "errors"
    "flag"
    "github.com/jrefior/run" // update this path for your filesystem
)

func main() {
    // do something to get configuration values
    portFlag := flag.Int("p", 0, "Override default listen port")
    logLevelFlag := flag.Int("l", 0, "Override default log level")
    flag.Parse()

    // put your runner options here
    runnerOpts := make([]func(*run.Runner) error, 0)

    // with flags, we're not sure if port was set by flag, so test
    if *portFlag > 0 {
        runnerOpts = append(runnerOpts, func(runner *run.Runner) error {
            if *portFlag < 1024 {
                return errors.New("Ports below 1024 are privileged")
            }
            runner.Port = *portFlag
            return nil
        })
    }
    if *logLevelFlag > 0 {
        runnerOpts = append(runnerOpts, func(runner *run.Runner) error {
            if *logLevelFlag > 8 {
                return errors.New("The maximum log level is 8")
            }
            runner.LogLevel = *logLevelFlag
            return nil
        })
    }
    // other configuration if applicable
    runner := run.NewRunner(runnerOpts...)
    runner.Run()
}

Example usage:

$ ./program -p 8987
Runner Configuration:
            Port                   8987
            Host              localhost
        LogLevel                      1
jrefior
  • 4,092
  • 1
  • 19
  • 29
  • Thanks you very much 1+, can you please provide some working example which I can test and use? like your second code snippet ?https://play.golang.org/ – Jenny Hilton Mar 06 '18 at 07:44
  • I try this code and it's not working...can you help please with dummy running example? – Jenny Hilton Mar 06 '18 at 08:03
  • The Go Playground is not a great place to write/test/run solutions to your question, because it does not support defining multiple files or multiple packages: everything has to be in `package main` or imported from the standard library ([list here](https://stackoverflow.com/questions/36409302/which-packages-may-be-imported-in-the-go-playground)). You could update your question to append the code that isn't working so I could help, or you could post it as a new question. If I get a chance later I can also try to write a runnable example but it probably won't be in the Go Playground. – jrefior Mar 06 '18 at 18:48
  • thanks, if you can share your `working` code it will be great – Jenny Hilton Mar 07 '18 at 11:03
  • @JennyHilton Added a runnable example – jrefior Mar 07 '18 at 16:15
  • It's looks ok but I couldnt check it. I give you the point you deserve it...but I've some question after I check it ...Thanks! – Jenny Hilton Mar 08 '18 at 09:11
  • I've opend another question with bounty please see if you can assit https://stackoverflow.com/questions/46511312/golang-template-with-switch-on-objcts – Jenny Hilton Apr 15 '18 at 11:04
  • 1
    @jrefior I'm conscious that this answer is quite old, but it's worth pointing out that the Go Playground can now simulate multiple packages/files: https://go.dev/play/p/B5dXwG6rOMs – jub0bs Oct 24 '22 at 12:17
  • 1
    @jub0bs Thanks that's good information to note/add to the comment thread! The Go Playground has evolved and improved! – jrefior Oct 25 '22 at 13:31
3

I use this to define per package Config Structs which are easier to manage and are loaded at the app start.

Define your config struct like this

type Config struct {
    Conf1               package1.Configuration        `group:"conf1"           namespace:"conf1"`
    Conf2               package2.Configuration        `group:"conf2"           namespace:"conf2"`
    Conf3               Config3                       `group:"conf3"           namespace:"conf3"`
    GeneralSetting      string                        `long:"Setting"          description:"setting"        env:"SETTING"      required:"true"`
}

type Config3 struct {
    setting string
}

And use "github.com/jessevdk/go-flags" to pass either --config3.setting=stringValue cmd arguments, or ENV variables export CONFIG3_SETTING=stringValue:

type Configuration interface {}

const DefaultFlags flags.Options = flags.HelpFlag | flags.PassDoubleDash

func Parse(cfg Configuration) []string {
    args, _ := flags.NewParser(cfg, DefaultFlags).Parse()

    return args
}

And your main should look something like this:

func main() {
    // Parse the configuration.
    var cfg Config
    Parse(&cfg)
    service := NewService(cfg.Conf3.Setting)
}
Alex Efimov
  • 3,335
  • 1
  • 24
  • 29