Interface-based functional optionals in Golang
Published: Nov 16, 2025
Last updated: Nov 16, 2025
This blog post covers the Functional Options Pattern described within 100 Go Mistakes and How To Avoid Them, then compares some popular Golang repositories for how they handle flexible constructors.
The Functional Optionals Pattern
According to the book, an example of the pattern is as follows:
type options struct { port *int } 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 } } // Usage example: server, err := httplib.NewServer( "localhost", httplib.WithPort(8080), httplib.WithTimeout(time.Second), )
An overview for the main idea behind the Functional Options Pattern is this (taken directly from the source):
- An unexported struct holds the configuration: options.
- Each option is a function that returns the same type:
type Option func(options *options) error. For example,WithPortaccepts an int argument that represents the port and returns an Option type that represents how to update the options struct.
This enables us to construct a NewServer while maintaining some flexibility and being wary of Golang Zero values:
func NewServer(addr string, opts ...Option) (*http.Server, error) { 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 } } // ... }
The first part of the "constructor" function applies the options, the second part handles an nil values to set defaults.
An interface-based option type
Wanting to understand more about "flexibility" with constructors in Golang, I looked to some popular open source projects and saw an approach with gRPC-Go and Uber's Zap logger that look to make use of an interface that has an apply function defined within it.
Interestingly enough, they both had an approach that made use of an interface with an apply method defined.
We'll show both case studies before refactoring the httplib example that we've defined so far.
Case Study: gRPC-Go
For example, in gRPC-Go:
type DialOption interface { apply(*dialOptions) } // and most options are implemented as functions that modify *dialOptions conn, err := grpc.NewClient( "localhost:50051", grpc.WithTransportCredentials(creds), grpc.WithBlock(), grpc.WithUnaryInterceptor(myInterceptor), )
With something like grpc.NewClient, it takes in a ...DialOption as the final variable argument within the NewClient definition:
func NewClient(target string, opts ...DialOption) (conn *ClientConn, err error) { cc := &ClientConn{ target: target, conns: make(map[*addrConn]struct{}), dopts: defaultDialOptions(), } cc.retryThrottler.Store((*retryThrottler)(nil)) cc.safeConfigSelector.UpdateConfigSelector(&defaultConfigSelector{nil}) cc.ctx, cc.cancel = context.WithCancel(context.Background()) // Apply dial options. disableGlobalOpts := false for _, opt := range opts { if _, ok := opt.(*disableGlobalDialOptions); ok { disableGlobalOpts = true break } } if !disableGlobalOpts { for _, opt := range globalDialOptions { opt.apply(&cc.dopts) } } // NOTE: We iterate through the range of opts and call their // `apply` method. for _, opt := range opts { opt.apply(&cc.dopts) } // ... code implementation omitted return cc, nil }
In the above implementation, we see that for each opt, the apply method is called.
// funcDialOption wraps a function that modifies dialOptions into an // implementation of the DialOption interface. type funcDialOption struct { f func(*dialOptions) } func (fdo *funcDialOption) apply(do *dialOptions) { fdo.f(do) } func newFuncDialOption(f func(*dialOptions)) *funcDialOption { return &funcDialOption{ f: f, } }
An example function that can be used for defining an option function:
func WithBlock() DialOption { return newFuncDialOption(func(o *dialOptions) { o.block = true }) }
Case Study: Uber Zap
The same can be seen with the way Uber Zap uses it's New constructor which takes a variable Option argument.
Within Option:
// An Option configures a Logger. type Option interface { apply(*Logger) } // optionFunc wraps a func so it satisfies the Option interface. type optionFunc func(*Logger) func (f optionFunc) apply(log *Logger) { f(log) }
The New constructor:
func New(core zapcore.Core, options ...Option) *Logger { if core == nil { return NewNop() } log := &Logger{ core: core, errorOutput: zapcore.Lock(os.Stderr), addStack: zapcore.FatalLevel + 1, clock: zapcore.DefaultClock, } return log.WithOptions(options...) } // WithOptions clones the current Logger, applies the supplied Options, and // returns the resulting Logger. It's safe to use concurrently. func (log *Logger) WithOptions(opts ...Option) *Logger { c := log.clone() for _, opt := range opts { opt.apply(c) } return c } // optionFunc wraps a func so it satisfies the Option interface. type optionFunc func(*Logger) func (f optionFunc) apply(log *Logger) { f(log) } // ... an example option // WithClock specifies the clock used by the logger to determine the current // time for logged entries. Defaults to the system clock with time.Now. func WithClock(clock zapcore.Clock) Option { return optionFunc(func(log *Logger) { log.clock = clock }) }
Reworking the httplib example code
Taking inspiration from the above, we can rework the example code taken from 100 Go Mistakes and How To Avoid Them to follow this interface-based approach.
In our case, we will still maintain the ability to return `error` to kept the same response type as before, however this may be omitted depending on your implementation. For example: if you handle all defaults, `error` would be unnecessary.
First, you define options struct to match what we saw before:
type options struct { port *int // timeout *time.Duration, etc... }
Next, we define the interface for Option:
// Option configures how we set up the server. type Option interface { apply(*options) error }
We then define our function-wrapper type:
// optionFunc wraps a func so it satisfies the Option interface. type optionFunc func(*options) error func (f optionFunc) apply(o *options) error { return f(o) }
Then we can declare our functional option functions:
func WithPort(port int) Option { return optionFunc(func(o *options) error { if port < 0 { return errors.New("port should be positive") } o.port = &port return nil }) }
With these declarations, we can then create our new constructor:
func NewServer(addr string, opts ...Option) (*http.Server, error) { var o options for _, opt := range opts { if err := opt.apply(&o); err != nil { return nil, err } } // existing port resolution logic... }
At this point, can still make use of the exact same signature as before:
// Usage example: server, err := httplib.NewServer( "localhost", httplib.WithPort(8080), // ... other supported options // httplib.WithTimeout(time.Second), )
A working example
If, instead of using http.Server we placed in our own struct Server for demonstration purposes, we can see a working example of this pattern with the following:
package main import ( "errors" "fmt" ) type options struct { port *int // timeout *time.Duration, etc... } // Option configures how we set up the server. type Option interface { apply(*options) error } // optionFunc wraps a func so it satisfies the Option interface. type optionFunc func(*options) error func (f optionFunc) apply(o *options) error { return f(o) } func WithPort(port int) Option { return optionFunc(func(o *options) error { if port < 0 { return errors.New("port should be positive") } o.port = &port return nil }) } type Server struct { addr string port int } func NewServer(addr string, opts ...Option) (*Server, error) { var o options for _, opt := range opts { if err := opt.apply(&o); err != nil { return nil, err } } srv := &Server{ addr: addr, port: *o.port, } return srv, nil } func main() { fmt.Println("=== Example 1: Valid usage ===") srv, err := NewServer("localhost", WithPort(8080), ) if err != nil { fmt.Println("unexpected error:", err) return } fmt.Printf("Server struct: %+v\n\n", srv) fmt.Println("=== Example 2: Invalid usage (error) ===") srv2, err := NewServer("localhost", WithPort(-5), // invalid: negative port ) if err != nil { fmt.Println("expected error:", err) return } fmt.Printf("Server struct: %+v\n", srv2) }
Running go run targeting this file prints the following:
$ go run main.go === Example 1: Valid usage === Server struct: &{addr:localhost port:8080} === Example 2: Invalid usage (error) === expected error: port should be positive
Conclusion
This blog post reviewed the Functional Options Pattern from 100 Go Mistakes and How To Avoid Them, before looking at some open-source case studies with gRPC-Go and Uber Zap.
We then refactored the example code to follow the interface-style approach with the apply function for composing options while maintaining the same capabilities to return errors for invalid option arguments.
Links and Further Reading
Photo credit: johnnyb803
Interface-based functional optionals in Golang
Introduction