Not using the functional options pattern

For API design, how do we handle optional configurations?

Mistake

Using struct parameters or complicated builders to handle optional configurations.

type Config struct {
	Port int
}

func NewServer(addr string, cfg Config) {
	// ...
}

// Initializes Port to 0
c1 := httplib.Config{
	Port: 0,
}

// Port is missing, so it’s initialized to 0.
c2 := httplib.Config{
}

Fix

Leverage closures and variadic arguments

type options struct {
	port *int // we make this a pointer so that it can be nil
}

// A function type that updates the options struct
type Option func(options *options) error

func WithPort(port int) Option {
	return func(options *options) error {
		if port < 0 {
			return errors.New("port should be positive")
		}

		options.port = &port

		return nil
	}
}

// Accepts many option closures
func NewServer(addr string, opts ...Option) (*http.Server, error) {
	// we create the options struct that the closures reference
	var options options

	for _, opt := range opts {
		err := opt(&options)
		if err != nil {
			return nil, err
		}
	}

	// At this stage, the options struct is built and contains the config
	// Therefore, we can implement our logic related to port configuration
	var port int
	if options.port == nil {
		port = defaultHTTPPort
	} else {
		if *options.port == 0 {
			port = randomPort()
		} else {
			port = *options.port
		}
	}
	// ...
}

References