I was looking at ways to do method chaining in Go and found this article from Jon Calhoun about functional options. He made some really good points. I read through it probably three times, and being the dull knife that I am had trouble wrapping my head around it. So I sat down and tried to figure it out. I figured I’d put this post together for anyone who is in the same boat as me and wants a nice simple example of how functional options work.
First let’s define a struct to hold some data. I’ve been using BoltDB recently so let’s create a struct for a BoltDB record and a function to return a new record.
package main
type Record struct {
Bucket []byte
Key []byte
Value []byte
}
func NewRecord() *Record {
return &Record {}
}
Now that we have a pointer to an empty record we can fill it with some values.
package main
type Record struct {
Bucket []byte
Key []byte
Value []byte
}
func NewRecord() *Record {
return &Record {}
}
func main() {
r := NewRecord()
r.Bucket = []byte("myBucket")
r.Key = []byte("myKey")
r.Value = []byte("somevalue")
}
I’ve always kind of thought this looked kind of bad but didn’t really know how to make it prettier, other than passing values into the NewRecord
function or just initializing the struct without the NewRecord
function. However, now I have an answer.
Functional options are what they sound like, they are options passed to a function that are also functions. So to create a new variable of type Record we can pass functions that set our data for us, but are optional unlike if they were just parameters.
We need to create a helper type and then the functions to modify our record struct:
package main
type Record struct {
Bucket []byte
Key []byte
Value []byte
}
type RecordOption func(*Record)
func NewRecord(opts ...RecordOption) *Record {
r := &Record{}
for _, opt := range opts {
opt(r)
}
return r
}
We changed NewRecord
to a variadic function that accepts our functional options. We can then range over the options to set our struct fields. Next let’s create our functions to modify our Record.
package main
type Record struct {
Bucket []byte
Key []byte
Value []byte
}
type RecordOption func(*Record)
func NewRecord(opts ...RecordOption) *Record {
r := &Record{}
for _, opt := range opts {
opt(r)
}
return r
}
func BucketName(name string) RecordOption {
return func(r *Record) {
r.Bucket = []byte(name)
}
}
func Key(name string) RecordOption {
return func(r *Record) {
r.Key = []byte(name)
}
}
func Value(value string) RecordOption {
return func(r *Record) {
r.Value = []byte(value)
}
}
We now have functions to modify the fields in our struct and then return our RecordOption type. The way we call these can look similar to method chaining, but is done as parameters in our function.
record2 := NewRecord(
BucketName("myBucket"),
Key("myKey"),
Value("somevalue"),
)
This looks much cleaner than assigning fields in each line after we call NewRecord()
. The following is an example of everything put together. The example includes a function to base64 our data for the value and then a non-existent method to write the record to the database (because I’m too lazy to type all of that out).
package main
import (
"encoding/base64"
"fmt"
)
type Record struct {
Bucket []byte
Key []byte
Value []byte
}
type RecordOption func(*Record)
func NewRecord(opts ...RecordOption) *Record {
r := &Record{}
for _, opt := range opts {
opt(r)
}
return r
}
func BucketName(name string) RecordOption {
return func(r *Record) {
r.Bucket = []byte(name)
}
}
func Key(name string) RecordOption {
return func(r *Record) {
r.Key = []byte(name)
}
}
func Value(value string) RecordOption {
return func(r *Record) {
r.Value = []byte(value)
}
}
func toBase64(key string) string {
encoded := base64.RawStdEncoding.EncodeToString([]byte(key))
return encoded
}
func main() {
data := `
{
color: "red",
value: "#f00"
}`
encoded := toBase64(data)
record := NewRecord(
BucketName("myBucket"),
Key("myKey"),
Value(encoded),
)
record.WriteRecord()
}
Hopefully this at least gives you a little better understanding of how functional options can be built and used in your development.