Last active
June 17, 2022 22:48
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ... | |
} |
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
Do you have suggestions for how to test this pattern or how to mock interfaces that follow this pattern?