Skip to content

Instantly share code, notes, and snippets.

@logrusorgru
Last active October 3, 2023 01:56
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save logrusorgru/0d0f4ac5d7b95f79b7884b5664658bc9 to your computer and use it in GitHub Desktop.
Save logrusorgru/0d0f4ac5d7b95f79b7884b5664658bc9 to your computer and use it in GitHub Desktop.
golang functional options problems

Functional options: https://gist.github.com/travisjeffery/8265ca411735f638db80e2e34bdbd3ae

In real world we need to validate input. This functional options kills global namespace. Take a look my average config:

import (
   "domain.tld/user/log"
)

// default configs
const (
    Host string = "127.0.0.1" // default host address
    Port int    = 8897          // default port
)

// A Config represents an average config for this comment
type Config struct {
    Log  log.Config
    Host string
    Port int
}

// Validate the Config values
func (c *Config) Validate() (err error) {
    if err = c.Log.Validate(); err != nil {
        return
    }
    if c.Host == "" {
        return ErrEmptyHost
    }
    if c.Port < 0 {
        return ErrNegtivePort
    }
    return
}

// NewConfig returns default configurations
func NewConfig() (c *Config) {
    c = new(Config)
    c.Log = log.NewConfig()
    c.Host = Host
    c.Port = Port
    return
}

// FromFlags obtains values from command-line flags. Call this method before `flag.Parse` and
// validate the Config after that
func (c *Config) FromFlags() {
    c.Log.FromFlags()
    flag.StringVar(&c.Host,
        "h",
        c.Host,
        "host address")
    flag.StringVar(&c.Port,
        "p",
        c.Port,
        "port")
}
  • the main problem is inventing new name for Host and Port constants

How to validate configs using the functional options?

func Option func(c *Config) error

// Host ... what I need to write here?
func Host(address string) Option {
    if address == "" {
        return func(*Config) error { return ErrEmptyAddress }
    }
    return func(c *Config) (_ error) {
        c.Host = address
       return
    }
}

But we also need to validate configs from flags

func validateHost(address string) (err error) {
	if address == "" {
		err = ErrEmptyAddress
	}
	return
}

func Host(address string) Option {
	return func(c *Config) (err error) {
		if err = validateHost(address); err != nil {
			return
		}
		c.Host = address
		return
	}
}

Let's see global namespace

Host
Port
Config
NewConfig

vs

defaultHost
defaultPort
validateHost
validatePort
Host
Port
Config
NewConfig

How to deal with the Config.Log?

@elisarver
Copy link

I'd solve the defaults problem two ways. This first is to provide a default to flag:

func (f *FlagSet) String(name string, value string, usage string) *string

(this is the value field)

The second thing is to validate your inputs before you get to injecting them as options. You'd probably call this validation from NewConfig.

If you use github.com/spf13/viper, you can blend flags, environment variables, and default config values for a mature set of fallbacks.

@logrusorgru
Copy link
Author

logrusorgru commented Mar 5, 2021

Ok, after few years. Put defaults in the NewOptions function. Instead of constants.

type Options struct {
    Host string `json:"host" yaml:"host" toml:"host" xml:"host" mapstructure:"host" `
    Port int `json:"port" yaml:"port" toml:"port" xml:"port" mapstructure:"port" `
}

// The NewOptions returns new default options.
func NewOptions() (o *Options) {
  o = new(Options)
  o.Host = "[::]"
  o.Port = 3000
  return
}

type Option(o *Options) (err error)

func (o *Options) apply(opts ...Options) (err error) {
    for opt := range  opts {
        if err = opt(o); err != nil {
            return
        }
    }
    return
}

Add a Validate function that uses the Option functions

// Validate the Options
func (o *Options) Validate() (err error) {
    err =  o.apply(
       Host(o.Host),
       Port(o.Port),
    )
    return
}

Where the Host and the Port are

func Host(host string) Option {
    return func(o *Options) (err error) {
        /// validate and return error
        // or
        o.Host = Host
        return
    }
}

func Port(port int) Option {
    return func(o *Options) (err error) {
        if port < 0 || port > 65535 {
            return fmt.Errorf("illegal port number: %d", port)
        }
        o.Port = port
        return
    }
}

We can fill them form flags and from config files. Then call the Validate. And can use the (opts ..Option) approach.

We got rid out of defaults in constants. The list of default values just moved from file heading into the NewOptions function. Using pkg.go.dev a end user can navigate into code and see. Yep, not in the document... A little lost. A project documentation, if it's a private library, for example, developer have to extract the defaults and put in package documentation. So, just keep creating open source projects to solve it without additional efforts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment