# Interface-based functional optionals in Golang

> A pattern to pass optional settings to build flexible constructors without a ton of parameters.

- Canonical: https://dennisokeeffe.com/blog/2025-11-16-interface-based-functional-options-in-golang
- Published: 2025-11-16
- Updated: 2025-11-16
- Tags: golang

This blog post covers the [Functional Options Pattern](https://100go.co/#not-using-the-functional-options-pattern-11) described within [100 Go Mistakes and How To Avoid Them](https://100go.co/), 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:

```go
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):

1. An unexported struct holds the configuration: options.
2. Each option is a function that returns the same type: `type Option func(options *options) error`. For example, `WithPort` accepts 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](https://go.dev/tour/basics/12):

```go
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](google.golang.org/grpc) and [Uber's Zap logger](https://github.com/uber-go/zap) 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:

```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](https://github.com/grpc/grpc-go/blob/v1.76.0/clientconn.go#L145) definition:

```go
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.

```go
// 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:

```go
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](https://github.com/uber-go/zap/blob/master/logger.go#L69) which takes a variable [Option](https://github.com/uber-go/zap/blob/master/options.go#L31) argument.

Within [Option](https://github.com/uber-go/zap/blob/master/options.go#L31):

```go
// 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](https://github.com/uber-go/zap/blob/master/logger.go#L69):

```go
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:

```go
type options struct {
    port *int
    // timeout *time.Duration, etc...
}
```

Next, we define the interface for `Option`:

```go
// Option configures how we set up the server.
type Option interface {
    apply(*options) error
}
```

We then define our function-wrapper type:

```go
// 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:

```go
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:

```go
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:

```go
// 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:

```go
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:

```sh
$ 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](https://100go.co/#not-using-the-functional-options-pattern-11), 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

- [Functional Options Pattern — 100 Go Mistakes](https://100go.co/#not-using-the-functional-options-pattern-11)
- [100 Go Mistakes and How To Avoid Them](https://100go.co/)
- [Golang Zero Values](https://go.dev/tour/basics/12)
- [gRPC-Go](https://google.golang.org/grpc)
- [gRPC-Go NewClient Implementation](https://github.com/grpc/grpc-go/blob/v1.76.0/clientconn.go#L145)
- [Uber Zap Logger](https://github.com/uber-go/zap)
- [Uber Zap Logger New Constructor](https://github.com/uber-go/zap/blob/master/logger.go#L69)
- [Uber Zap Option Interface](https://github.com/uber-go/zap/blob/master/options.go#L31)

_Photo credit: [`johnnyb803`](https://unsplash.com/@johnnyb803)_
