About

This post looks at the functional options pattern and how it can help you write Go programs that can initialize structs in idiomatic Go. The pattern is simple but clever and it can help you design beautiful Go libraries and interfaces.

Background

Context

As I continued to learn Go I found myself wanting to write Go that used patterns I had discovered in libraries like goldmark (a markdown library). At first, the pattern we will discuss was a mystery that I took note of but had no idea how it worked. Eventually I started to build my own libraries, and I wanted to design idiomatic APIs that were nice to use. That's when I started to research what's known as the functional options pattern.

Pattern

The functional options pattern is a powerful and idiomatic approach to initializing and configuring Go structs. It provides a flexible, readable, and extensible API for setting struct properties.

The pattern typically revolves around a New function, and it usually belongs to a package other than main. The New function accepts a variable number of Option arguments, where Option is a function type defined to receive a pointer to the struct being configured (eg type Option func(*User)).

The Option values are created by option functions (like SetName or SetAge) that are exported by a package. Each of these functions takes a desired configuration value, then returns a new Option function. This returned Option function "closes over" the provided value, and when the New function iterates and calls it, the function will set a property on the struct.

The design allows for easy addition of new configuration parameters without altering the New function's signature, and it also provides a mechanism to set default struct properties that can then be overriden by the caller.

Example

Context

The following example illustrates the functional options pattern described in the previous section by defining a User struct and a collection of public package functions that configure the User struct:

package user

type User struct {
  name string
  age  uint8
}

type Option func(*User)

func New(opts ...Option) *User {
  u := &User{name: "unknown", age: uint8(0)}
  for _, set := range opts {
    set(u)
  }
  return u
}

func SetName(name string) Option {
  return func(u *User) {
    u.name = name
  }
}

func SetAge(age uint8) Option {
  return func (u *User) {
    u.age = age
  }
}
package main

import (
  "user"
)

func main() {
  u := user.New(
    user.SetName("Mario"),
    user.SetAge(uint8(37))
  )
}

Explanation

The user package defines a User struct and an Option type for configuring it. Its New constructor initializes a User with default values, then applies any provided Option functions, such as SetName and SetAge. The main function then uses user.New with these options to create a User named 'Mario' (age 37) and in turn that overrides the default values.

Conclusion

The functional options pattern makes it easy to build APIs that are both flexible and enjoyable to use. By separating configuration from initialization, we can evolve our types without breaking interfaces. Personally, I’ve found this pattern to be one of the most useful tools in my Go toolkit. If you haven't already, I encourage you to give it a try in your next project.