Functional Options Pattern

Roaming Roadster
3 min readDec 2, 2023

--

Have you ever wanted to create an object, but needed to pass a lot of parameters? For example, you may want to create a client and need to pass parameters such asport, retry, timeout, etc.:

type Client struct {
port string
retry int
timeout int
}

func New(port string, retry int, timeout int) *Client {
return &Client{port, retry, timeout}
}

You can create a client like this:

myclient := client.New("1234", 3, 5)

You need to pass all the parameters required in New function. What if there are a lot of parameters and some of them are optional? What if you want to add a new parameter but don’t want to break other services that use this struct? There are a couple of way to solve this issue but each of them has pros and cons. Let’s go through the solutions and see what use cases they are suitable for.

Solution 1. Define multiple “New” functions

You can create a New function for each option combination you want:

func New(port string, retry int, timeout int) *Client {
return &Client{port, retry, timeout}
}

func NewWithPort(port string) *Client {
return New(port, 3, 5)
}

func NewWithTimeout(port string, timeout int) *Client {
return New(port, 3, timeout)
}

In this example, port is required, and retry and timeout are optional.

In the future, if you want to add a new option, you can add a corresponding new function without breaking existing code.

Pros:

  • easy to understand.
  • no breaking changes if you want to add a new option.

Cons:

  • need to define multiple new functions.

Solution 2. Create Config struct for options

Another approach is to create aConfig struct for your options:

package client

type Client {
cfg Config
}

type Config struct {
port string
retry int
timeout int
}

func New(cfg Config) *Client {
return &Client{cfg}
}

You can create a client like this:

myclient := client.New(Config{"1234", 3, 5})

This is a widely used approach since it organizes all configurations in a struct.

Pros:

  • No need to define multiple new functions.

Cons:

  • Need to make breaking changes if we want to add a new option in Config.

Solution 3. Functional Options

Functional Options pattern allows you create an object and pass optional parameters only if you need to. Let’s see how it works.

package client

const (
defaultPort = "8080"
defaultRetry = 3
defaultTimeout = 5
)

type Client {
port int
retry int
timeout int
}

func New(options ...func(*Client)) *Client {
myclient := &Client{
port: defaultPort,
retry: defaultRetry,
timeout: defaultTimeout,
}

for _, opt := range options {
opt(myclient)
}

return myclient
}

func WithTimeout(timeout int) func(*Client) {
return func(s *Client) {
s.timeout = timeout
}
}

Then you can create a client like this:

myclient := Client.New(
client.WithTimeout(5),
)

You can see that the New function accepts multiple parameters, and each parameter is a function that set one of the options for your client.

Pros:

  • No need to pass all parameters to create an object.
  • Functional options are flexible. If you want to add a new variable to your Client, you can define a new With function and won’t break existing code.
  • You can call With function in any order.

Cons:

  • Code may look complicated if you are not familiar with the functional options pattern.
  • Need to create functions for new options.

When to use functional options?

Functional options provide you a way to manage the options for your struct, but it can look complicated in the first place. Before you use anything, you should understand your needs and see if it can help you address you needs. There are two main reasons that I think particularly suitable for functional options:

  • you are working on a struct that has a lot of optional variables. Usually this happens if your struct needs a lot of configurations, such as a http server or client.
  • you are working on a library that will be used by others. In this case, you don’t want to make breaking changes to others unless it is needed.

Don’t make things over-complicated if you don’t need to. Hopefully this guide can help you understand functional options pattern, and how you can manage your configs and the pros and cons for each approach!

--

--