Suppose, you have a function that takes an option struct and a message as input. Then it stylizes the message according to the option fields and prints it. What’s the most sensible API you can offer for users to configure your function? Observe:

// app/src
package src

// Option struct
type Style struct {
    Fg string // ANSI escape codes for foreground color
    Bg string // Background color
}

// Display the message according to Style
func Display(s *Style, msg string) {}

In the src package, the function Display takes a pointer to a Style instance and a msg string as parameters. Then it decorates the msg and prints it according to the style specified in the option struct. In the wild, I’ve seen 3 main ways to write APIs that let users configure options:

  • Expose the option struct directly
  • Use the option constructor pattern
  • Apply functional option constructor pattern

Each comes with its own pros and cons.

Expose the option struct

In this case, you’d export the Style struct with all its fields and let the user configure them directly. The previous snippet already made the struct and fields public. From another package, you could import the src package and instantiate Style like this:

package main
import "app/src"

// Users instantiate the option struct
c := &src.Style{
    "\033[31m", // Maroon
    "\033[43m", // Yellow
}

// Then pass the struct to the function
Display(c, "Hello, World!")

To configure option fields, mutate the values in place:

c.Fg = "\033[35m" // Magenta
c.Bg = "\033[40m" // Black

This works but will break users’ code if new fields are added to the option struct. But your users can instantiate the struct with named parameters to avoid breakage:

c := &src.Style{
    Fg: "\033[31m", // Maroon
                   // Bg will be implicitly set to an empty string
}

In this case, the field that wasn’t passed would assume the corresponding zero value. For instance, Bg will be initialized as an empty string. However, this pattern puts the responsibility of retaining API compatibility on the users’ shoulders. So if your code is meant for external use, there are better ways to achieve option configurability.

Option constructor

Go standard library extensively uses this pattern. Instead of letting the users instantiate Style directly, you expose a NewStyle constructor function that constructs the struct instance for them:

package src
// same as before

// NewStyle option constructor instantiates a Style instance
func NewStyle(fg, bg string) *Style {
    return &Style{fg, bg}
}

It’ll be used as follows:

package main
import "app/src"

// The users will now use NewStyle to instantiate Style
c := src.NewStyle(
    "\033[31m", // Maroon
    "\033[43m", // Yellow
)
Display(c, "Hello, World!")

If a new field is added to Style, update NewStyle to have a sensible default value for it or initialize the struct with named parameters to set the optional fields to their respective zero values. This avoids breaking users’ code as long as the constructor function’s signature doesn’t change.

package src

type Style struct {
    Fg string
    Bg string
    Und bool // Underline or not
}

// Function signature unchanged though new option field added
// Set sensible default in constructor function
func NewStyle(fg, bg string) *Style{
    return &Style{
        Fg: fg, Bg: bg, // Und will be implicitly set to false
    }
}

In NewStyle, we implicitly set the value of Und to false but you can be explicit there depending on your needs. The struct fields can be updated in the same manner as before:

package main

c := src.NewStyle(
    "\033[31m", // Maroon
    "\033[43m", // Yellow
)
c.Und = true // Default is false, we're setting it to true
src.Display(c, "Hello, World!")

This should cover most use cases. However, if you don’t want to export the underlying option struct, or your struct has tons of optional fields requiring extensibility, you’ll need an extra layer of indirection to avoid the need to accept a zillion config parameters in your option constructor.

Functional option constructor

As mentioned at the tail of the last section, this approach works better when your struct contains many optional fields and you need your users to be able to configure them if they want. Go doesn’t allow setting non-zero default values for struct fields. So an extra level of indirection is necessary to let the users configure them. This approach also allows us to make the option struct private so that there’s no ambiguity around API usage.

Let’s say style now has two optional fields und and zigzag that allows users to decorate the message string with underlines or zigzagged lines:

package src

type style struct {
    fg string
    bg string
    und bool // Optional field
    zigzag bool // Optional field
}

Now, we’ll define a new type called styleoption like this:

// package src
type styleoption func(*style)

The styleoption function accepts a pointer to the option struct and updates a particular field with a user-provided value. The implementation of this type would look as such:

func (s *style) {s.fieldName = fieldValue}

Next, we’ll need to define a higher order config function for each optional field in the struct where the function will accept the field value and return another function with the styleoption signature. The WithUnd and WithZigzag wrapper functions will be a part of the public API that the users will use to configure style:

// We only define config functions for the optional fields
func WithUnd(und bool) styleoption {
    return func(s *style) {
        s.und = und
    }
}

func WithZigzag(zigzag bool) styleoption {
    return func(s *style) {
        s.zigzag = zigzag
    }
}

Finally, our option constructor function needs to be updated to accept variadic options. Observe how we’re looping through the options slice and applying the field config functions to the struct pointer:

func NewStyle(fg, bg string, options ...styleoption) *style {
    s := &style{fg: fg, bg: bg} // und and zigzag are set to false

    // Apply all the styleoption functions returned from
    // field config functions.
    for _, opt := range options {
        opt(s)
    }
    return s
}

The users will use the code like this to instantiate style and update the optional fields:

c := src.NewStyle(
    "\033[31m",
    "\033[43m",
    src.WithUnd(true), // Default is false, but we're setting it to true
    src.WithZigzag(true), // Default is false
)

The required fields fg and bg must be passed while constructing the option struct. The optional fields can be configured with the field config functions like WithUnd and WithZigzag.

The complete snippet looks as follows:

package src

// We can keep the option struct private
type style struct {
    fg string
    bg string
    und bool // Optional field
    zigzag bool // Optional field
}

// This can be private too since the users won't need it directly
type styleoption func(*style)

// We only define public config functions for the optional fields
func WithUnd(und bool) styleoption {
    return func(s *style) {
        s.und = und
    }
}

func WithZigzag(zigzag bool) styleoption {
    return func(s *style) {
        s.zigzag = zigzag
    }
}

// Options are variadic but the required fiels must be passed
func NewStyle(fg, bg string, options ...styleoption) *style {
    // You can also initialize the optional values explicitly
    s := &style{fg: fg, bg: bg}
    for _, opt := range options {
        opt(s)
    }
    return s
}

I first came across this pattern in Rob Pike’s blog1 on the same topic.

Verdict

While the functional constructor pattern is the most intriguing one among the three, I almost never reach for it unless I need my users to be able to configure large option structs with many optional fields. It’s rare and the extra indirection makes the code inscrutable. Also, it renders the IDE suggestions useless.

In most cases, you can get away with exporting the option struct Stuff and a companion function NewStuff to instantiate it. For another canonical example, see bufio.Read and bufio.NewReader in the standard library.

Recent posts

  • TypeIs does what I thought TypeGuard would do in Python
  • ETag and HTTP caching
  • Crossing the CORS crossroad
  • Dysfunctional options pattern in Go
  • Einstellung effect
  • Strategy pattern in Go
  • Anemic stack traces in Go
  • Retry function in Go
  • Type assertion vs type switches in Go
  • Patching pydantic settings in pytest