Skip to content

Instantly share code, notes, and snippets.

@logrusorgru
Last active October 3, 2023 01:56
Show Gist options
  • 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?

@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