Golang provides a fantastic way of writing functions called Functional Options. This is a design pattern that allows callers to pass a set of options to a function, each represented by a function. Super flexible, and easy to use.
This is the n+1th article about Golang functional options but there’s a reason for that. It’s a very useful pattern. This pattern allows a lot of flexibility when writing functions, as each option can be composed together in any combination desired.
The problem #
Consider a function called TakeAWalk
that takes zero, one or multiple Dog
as parameter, and we also pass the
destination. The Dog
struct has a Name
field.
Without functional options, the function would look something like this:
type Dog struct {
Name string
}
func TakeAWalk(destination string, dogs ...Dog) {
// Do something with the dogs and destination
// for _, d := range dogs {
// fmt.Println("Dog name:", d.Name)
// }
// Do something with the destination
// fmt.Println("Destination:", destination)
}
Until this point it’s pretty simple. But what if we want to add more parameters to the function?
For example, I go to the mountains and I love it so much, next time I want to repeat and take a walk together with my girlfriend, bringing our dogs, and we also want to bring a ball.
In this case we have to add new parameters to the function, and the function signature would start to look embarrassing.
There’s a better way to do this, let’s see how.
Prepare the ground #
First, extend our code with a Walk
struct, that contains the options for the “walk”. Create a method called Go
that
represents taking the walk.
type Dog struct {
Name string
}
type Walk struct {
Dogs []Dog
Destination string
WithGirlfriend bool
BallColor string
}
func (w *Walk) Go() {
// Do something with the options
if w.Destination == "" {
// Set a default destination
w.Destination = "The park"
}
fmt.Println("I'm walking to:", w.Destination)
if w.WithGirlfriend {
fmt.Println("With my girlfriend")
}
if w.Dogs != nil {
fmt.Println("With my dogs:")
for _, dog := range w.Dogs {
fmt.Println("-", dog.Name)
}
}
if w.BallColor != "" {
fmt.Println("And we are bringing a ball colored:", w.BallColor)
}
}
Using this struct, define the a walk
object and call its Go
method:
func main() {
d1 := Dog{Name: "Pure"}
d2 := Dog{Name: "Gerbaud"}
walk := &Walk{
Destination: "The mountains",
WithGirlfriend: true,
Dogs: []Dog{d1, d2},
BallColor: "red",
}
walk.Go()
}
Getting better, but if we want to modify an attribute of the walk
object, we have to modify it outside.
To solve this, we can use functional options.
Functional options #
The basics #
Define functions that return a function that modifies the Walk
struct. These functions are usually called With*
functions.
func WithDog(dog Dog) func(w *Walk) {
return func(w *Walk) {
w.Dogs = append(w.Dogs, dog)
}
}
Code cleanup #
Let’s make the code cleaner: define a new type called Option
that represents all the functions that modify the Walk
struct.
Use this Option
to define the With*
functions.
type Option func(w *Walk)
func WithDog(dog Dog) Option {
return func(w *Walk) {
w.Dogs = append(w.Dogs, dog)
}
}
func WithDestination(destination string) Option {
return func(w *Walk) {
w.Destination = destination
}
}
func WithGirlfriend() Option {
return func(w *Walk) {
w.WithGirlfriend = true
}
}
func WithBall(color string) Option {
return func(w *Walk) {
w.BallColor = color
}
}
Using the options #
And lastly, we have to update the Go
method to accept Option
parameters.
func (w *Walk) Go(opts ...Option) {
// Apply the options to the WalkOptions struct
for _, opt := range opts {
opt(w)
}
// Do something with the options
}
Now we can call the Go
method like this:
func main() {
d1 := Dog{Name: "Pure"}
d2 := Dog{Name: "Gerbaud"}
// Initialize a new empty Walk
walk := &Walk{}
walk.Go(WithDog(d1), WithDog(d2), WithDestination("the beach"))
fmt.Println()
// Initialize a new empty Walk
walk = &Walk{}
walk.Go(WithDestination("the mountains"))
fmt.Println()
// Reuse the previous walk, but this time with my girlfriend, dogs and a ball
walk.Go(WithGirlfriend(), WithDog(d1), WithDog(d2), WithBall("red"))
}
And the output would be:
I'm walking to: the beach
With my dogs:
- Pure
- Gerbaud
I'm walking to: the mountains
I'm walking to: the mountains
With my girlfriend
With my dogs:
- Pure
- Gerbaud
And we are bringing a ball colored: red
Program exited.
Example Code #
The final form of the code will look like this:
package main
import (
"fmt"
)
func main() {
d1 := Dog{Name: "Pure"}
d2 := Dog{Name: "Gerbaud"}
// Initialize a new empty Walk
walk := &Walk{}
walk.Go(WithDog(d1), WithDog(d2), WithDestination("the beach"))
fmt.Println()
// Initialize a new empty Walk
walk = &Walk{}
walk.Go(WithDestination("the mountains"))
fmt.Println()
// Reuse the previous walk, but this time with my girlfriend, dogs and a ball
walk.Go(WithGirlfriend(), WithDog(d1), WithDog(d2), WithBall("red"))
}
func (w *Walk) Go(opts ...Option) {
// Apply the options to the WalkOptions struct
for _, opt := range opts {
opt(w)
}
// Do something with the options
if w.Destination == "" {
// Set a default destination
w.Destination = "The park"
}
fmt.Println("I'm walking to:", w.Destination)
if w.WithGirlfriend {
fmt.Println("With my girlfriend")
}
if w.Dogs != nil {
fmt.Println("With my dogs:")
for _, dog := range w.Dogs {
fmt.Println("-", dog.Name)
}
}
if w.BallColor != "" {
fmt.Println("And we are bringing a ball colored:", w.BallColor)
}
}
type Option func(w *Walk)
func WithDog(dog Dog) Option {
return func(w *Walk) {
w.Dogs = append(w.Dogs, dog)
}
}
func WithDestination(destination string) Option {
return func(w *Walk) {
w.Destination = destination
}
}
func WithGirlfriend() Option {
return func(w *Walk) {
w.WithGirlfriend = true
}
}
func WithBall(color string) Option {
return func(w *Walk) {
w.BallColor = color
}
}
type Dog struct {
Name string
}
type Walk struct {
Dogs []Dog
Destination string
WithGirlfriend bool
BallColor string
}
Conclusion #
In this way, functional options provide an excellent way of writing flexible functions in Golang.