Go program pattern 01: Functional Options Pattern
Tags: go, programming
Published: 2024-04-23 07:51:27

Go is not a fully object-oriented language, and some object-oriented patterns are not well-suited for it. However, over the years, Go has developed its own set of patterns. Today, I would like to introduce a common pattern: the Functional Options Pattern.

> This article is first published in the medium MPP plan. If you are a medium user, please follow me in [medium](https://medium.hxzhouh.com/). Thank you very much.

## What is the Functional Options Pattern?

Go does not have constructors like other languages. Instead, it typically uses a `New` function to act as a constructor. However, when a structure has many fields that need to be initialized, there are multiple ways to do so. One preferred way is to use the Functional Options Pattern.

The Functional Options Pattern is a pattern for constructing structs in Go. It involves designing a set of expressive and flexible APIs to help configure and initialize the struct.

The [Go Language Specification](https://github.com/uber-go/guide/blob/master/style.md#functional-options) by Uber mentions this pattern:

> Functional options are a pattern in which you declare an opaque `Option` type that records information in some internal structure. You accept these variable numbers of options and operate on the complete information recorded by the options on the internal structure.
> 
> Use this pattern for optional parameters in constructors and other public APIs where you expect these parameters to be extended, especially when there are already three or more parameters on these functions.

## An Example

To better understand this pattern, let's walk through an example.  
Let's define a `Server` struct:
```go
package main

type Server struct {
    host string
    port int
}

func New(host string, port int) *Server {
    return &Server{
        host,
        port,
    }
}

func (s *Server) Start() error {
    return nil
}
```
How do we use it?
```go
func main() {
    svr := New("localhost", 1234)
    if err := svr.Start(); err != nil {
        log.Fatal(err)
    }
}
```

But what if we want to extend the configuration options for the `Server`? There are generally three approaches:
-   Declare a new constructor function for each different configuration option.
-   Define a new `Config` struct to store the configuration information.
-   Use the Functional Options Pattern.
### Approach 1: Declare a new constructor function for each different configuration option

This approach involves defining dedicated constructor functions for different options. Let's say we added two fields to the `Server` struct:

```go
type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}
```

Typically, `host` and `port` are required fields, while `timeout` and `maxConn` are optional. We can keep the original constructor function and assign default values to these two fields:

```go
func New(host string, port int) *Server {
    return &Server{
        host,
        port,
        time.Minute,
        100,
    }
}
```

Then, we can provide two additional constructor functions for `timeout` and `maxConn`:

```go
func NewWithTimeout(host string, port int, timeout time.Duration) *Server {
    return &Server{
        host,
        port,
        timeout,
        100,
    }
}

func NewWithTimeoutAndMaxConn(host string, port int, timeout time.Duration, maxConn int) *Server {
    return &Server{
        host,
        port,
        timeout,
        maxConn,
    }
}
```

This approach works well for configurations that are unlikely to change frequently. Otherwise, you would need to create new constructor functions every time you need to add a new configuration. This approach is used in the Go standard library, such as the `Dial` and `DialTimeout` functions in the `net` package:

```go
func Dial(network, address string) (Conn, error)
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
```

### Approach 2: Use a dedicated configuration struct

This approach is also common, especially when there are many configuration options. Typically, you create a `Config` struct that contains all the configuration options for the `Server`. This approach allows for easy extension without breaking the API of the `Server`, even when adding more configuration options in the future.

```go
type Server struct {
    cfg Config
}

type Config struct {
    Host    string
    Port    int
    Timeout time.Duration
    MaxConn int
}

func New(cfg Config) *Server {
    return &Server{
        cfg,
    }
}
```

When using this approach, you need to construct a `Config` instance first, which brings us back to the original problem of configuring the `Server`. If you modify the fields in `Config`, you may need to define a constructor function for `Config` if the fields are changed to private.

### Approach 3: Use the Functional Options Pattern

A better solution is to use the Functional Options Pattern.

In this pattern, we define an `Option` function type:

```go
type Option func(*Server)
```

The `Option` type is a function type that takes a `*Server` parameter. Then, the constructor function for `Server` accepts a variable number of `Option` types as parameters:

```go
func New(options ...Option) *Server {
    svr := &Server{}
    for _, f := range options {
        f(svr)
    }
    return svr
}
```

How do the options work? We need to define a series of related functions that return `Option`:

```go
func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConn(maxConn int) Option {
    return func(s *Server) {
        s.maxConn = maxConn
    }
}
```

To use this pattern, the client code would look like this:

```go
package main

import (
    "log"

    "server"
)

func main() {
    svr := New(
        WithHost("localhost"),
        WithPort(8080),
        WithTimeout(time.Minute),
        WithMaxConn(120),
    )
    if err := svr.Start(); err != nil {
        log.Fatal(err)
    }
}
```

Adding new options in the future only requires adding corresponding `WithXXX` functions.

This pattern is widely used in third-party libraries, such as `github.com/gocolly/colly`:

```go
type Collector struct {
    // ...
}

func NewCollector(options ...CollectorOption) *Collector

// Defines a series of CollectorOptions
type CollectorOption struct {
    // ...
}

func AllowURLRevisit() CollectorOption
func AllowedDomains(domains ...string) CollectorOption
...
```

However, when Uber's Go Programming Style Guide mentions this pattern, it suggests defining an `Option` interface instead of an `Option` function type. This `Option` interface has an unexported method, and the options are recorded in an unexported `options` struct.

Can you understand Uber's example?

```go
type options struct {
    cache  bool
    logger *zap.Logger
}

type Option interface {
    apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
    opts.cache = bool(c)
}

func WithCache(c bool) Option {
    return cacheOption(c)
}

type loggerOption struct {
    Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
    opts.logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
    return loggerOption{Log: log}
}

// Open creates a connection.
func Open(
    addr string,
    opts ...Option,
) (*Connection, error) {
    options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
    }

    for _, o := range opts {
        o.apply(&options)
    }

    // ...
}
```

## Summary

In real-world projects, when dealing with a large number of options or options from different sources (e.g., from files or environment variables), consider using the Functional Options Pattern.

Note that in actual work, we should not rigidly apply the pattern as described above. For example, in Uber's example, the `Open` function does not only accept a variable number of `Option` parameters because the `addr` parameter is required. Therefore, the Functional Options Pattern is more suitable for cases with many configurations and optional parameters.

References:

-   [https://golang.cafe/blog/golang-functional-options-pattern.html](https://golang.cafe/blog/golang-functional-options-pattern.html)
-   [https://github.com/uber-go/guide/blob/master/style.md#functional-options](https://github.com/uber-go/guide/blob/master/style.md#functional-options)

If you found my article enjoyable, feel free to follow me and give it a 👏. Your support would be greatly appreciated.