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:
- user/user.go
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
}
}
- cmd/main/main.go
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.