Skip to content

Instantly share code, notes, and snippets.

@wtask
Last active June 17, 2022 22:48
Show Gist options
  • Save wtask/fe18ceecc2d27295138f59ccdf075b78 to your computer and use it in GitHub Desktop.
Save wtask/fe18ceecc2d27295138f59ccdf075b78 to your computer and use it in GitHub Desktop.
Golang functional options pattern by Dave Cheney with little improvments due to practice.
// optionsptrn - inspired by https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
package optionsptrn
import (
"errors"
"time"
// ...
)
// service - to implement SomeInterface.
type service struct{
// optional dependencies
timeout time.Duration
// ...
}
// serviceOption - high order func to setup single dependency.
// A little different from "func(*service) error" suggested by Dave.
type serviceOption func() (func(*service), error)
// setup - helper to set optional dependencies.
func (s *service) setup(options ...serviceOption) error {
// today, setup will be repeated for every service with options :(
if s == nil {
return nil
}
for _, option := range options {
if option == nil {
continue
}
setter, err := option()
if err != nil {
return err
}
if setter != nil {
setter(s)
}
}
return nil
}
// failedOption - helper to expose error from option builder
func failedOption(err error) serviceOption {
return func() (func(*service), error) {
return nil, err
}
}
// properOption - helper to expose valid setter from option builder
func properOption(setter func(*service)) serviceOption {
return func() (func(*service), error) {
return setter, nil
}
}
// NewService - package-level bulder of SomeInterface
func NewService(option ...serviceOption) (SomeInterface, error) {
s := &service{
timeout: 1*time.Second,
// other defaults ...
}
if err := s.setup(option...); err != nil {
return nil, err
}
// ...
return s, nil
}
// WithTimeout - setup timeout value for service
func WithTimeout(timeout time.Duration) serviceOption {
// we validate all paramteres for the option in single place
if timeout < 0 {
return failedOption(errors.New("invalid timeout"))
}
return properOption(func(s *service) {
s.timeout = timeout
})
}
// service methods to implement SomeInterface ...
package main
import "optionsptrn"
func main() {
// if you prepared multiple options, you can pass its in any order...
// only do not get carried away with options and do not confuse options with required parameters
s, err := optionsptrn.NewService(optionsptrn.WithTimeout(0))
if err != nil {
panic(err)
}
// start to use SomeInterface ...
}
@jaybrownlee
Copy link

Do you have suggestions for how to test this pattern or how to mock interfaces that follow this pattern?

@wtask
Copy link
Author

wtask commented Jun 5, 2019

If your package use this pattern, you can write package level tests which check fields of struct type are properly initilized when using options. It is easily. Also, there are not any interfaces here to think about mocks.

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