Override Defaults
Overriding default values in an object
One of the patterns or idioms I’ve enjoyed in Go is the concept of a NewType() constructor function building and returning an instance of your struct.
Without classes in Go, developers using our package would have to ensure they construct an object instance properly. Using the above pattern however, we ensure it’s done in a uniform way each time.
However what if we want to override some default assigned value, such as a Logger, or an HTTP client, with an implementation of our choosing? One way I’ve really come to enjoy is where we provide various Override methods and pass them to the NewType() constructor. I’ve seen this done where an Option type is declared as a function, and that’s what our override functions return. We then accept a variable length array of Option’s in NewType() and apply them to our struct before returning it.
As I’ve learned more about Go this does seem like a type of decorator pattern, but I’m not entirely sure it is, as examples I’ve seen are quite a bit different. If anyone can confirm/deny, I’d certainly appreciate it!
I’ve placed a full code example at the bottom, as well as instructions to run it!
This is a purposefully simple and contrived example, hopefully showing the pattern, and not getting too lost in the details. We’re going to have a main Person object with shirt and pants objects. The shirt and pants get default values, but we can override them if we want.
Explaining the parts
To start we have our Person object with a Shirt and Pants:
type Person struct {
shirt Shirt
pants Pants
}
Next we have a type that we use to override default fields/behavior. This says that Option is a type name, which is a function that accepts a pointer to a Person, and potentially returns an error.
type Option func(p *Person) error
To create Person objects in a uniform way, we have a constructor function:
func NewPerson(opts ...Option) (*Person, error) {
// All new Person's default to "XL" shirt/pant sizes
// and "Black" for the colors
p := &Person{
shirt: Shirt{
size: "XL",
color: "Black",
},
pants: Pants{
size: "XL",
color: "Black",
},
}
// If they provided any options to override defaults apply them now
// Wrap and return an error if any option function fails
for _, option := range opts {
if err := option(p); err != nil {
return nil, errors.Wrap(err, "error applying option")
}
}
// Everything went well, return the pointer to their Person object
return p, nil
}
NOTE: Using the type name/alias Option makes it easier to read our function definitions. Just remember functions are first class objects in Go, and we can call them like any other function, which we do in option(p)
We then declare our Shirt object, as well as functions that return an Option and allow us to override default values/behavior.
type Shirt struct {
size string
color string
}
func OverrideShirtSize(size string) Option {
return func(p *Person) error {
if size == "" {
return errors.New("shirt size cannot be empty")
}
p.shirt.size = size
return nil
}
}
func OverrideShirtColor(color string) Option {
return func(p *Person) error {
if color == "" {
return errors.New("shirt color cannot be empty")
}
p.shirt.color = color
return nil
}
}
The Override... functions can be odd initially. Here’s how I’d break it down: when we call the function OverrideShirtSize we get an Option value returned. Since that value needs to be a function that accepts a pointer to a Person and returns an error, we simply return an anonymous function/closure that satisfies those requirements; this could be any function it doesn’t have to be an anonymous one, it just has to satisfy the Option type requirements. This anonymous function/closure will capture the passed in variable either size or color. So if we assign the Option returned from OverrideShirtSize to a variable, we can later call it like any other function, which will call our anonymous function/closure to actually assign the passed in value to our object. Thus overriding our defaults with our desired values.
Our Pants object is very similar to our Shirt and has similar override functions:
type Pants struct {
size string
color string
}
func OverridePantsSize(size string) Option {
return func(p *Person) error {
if size == "" {
return errors.New("pants size cannot be empty")
}
p.pants.size = size
return nil
}
}
func OverridePantsColor(color string) Option {
return func(p *Person) error {
if color == "" {
return errors.New("pants color cannot be empty")
}
p.pants.color = color
return nil
}
}
Last we implement the fmt.Stringer interface, so we can call .String() on our object and print out its contents in a uniform way.
func (p *Person) String() string {
return fmt.Sprintf("Shirt size: %s, color: %s. Pants size: %s, color: %s.",
p.shirt.size, p.shirt.color, p.pants.size, p.pants.color)
}
With all this setup, we can now create new instances of our type. To create a completely default one we’d use:
person, err := NewPerson()
if err != nil {
panic("failed to create new Person")
}
fmt.Println(person.String())
The output is:
➜ ~/Learning/GoTests/overridedefaults go run .
Shirt size: XL, color: Black. Pants size: XL, color: Black.
We can create one that overrides shirt defaults doing the following:
redXXLShirt, err := NewPerson(OverrideShirtSize("XXL"), OverrideShirtColor("Red"))
if err != nil {
panic("failed to create new red XXL shirt Person")
}
fmt.Println(redXXLShirt.String())
The output is:
➜ ~/Learning/GoTests/overridedefaults go run .
Shirt size: XXL, color: Red. Pants size: XL, color: Black.
We can create one that overrides pants defaults doing the following:
whiteMPants, err := NewPerson(OverridePantsSize("M"), OverridePantsColor("White"))
if err != nil {
panic("failed to create new white M pants Person")
}
fmt.Println(whiteMPants.String())
The output is:
➜ ~/Learning/GoTests/overridedefaults go run .
Shirt size: XL, color: Black. Pants size: M, color: White.
If you have certain defaults you need to override frequently, they can be captured and passed in to all the objects you create:
opts := []Option{
OverrideShirtSize("L"),
OverrideShirtColor("White"),
OverridePantsSize("34"),
OverridePantsColor("Blue"),
}
overridenPerson, err := NewPerson(opts...)
if err != nil {
panic("failed to create new overriden Person")
}
fmt.Println(overridenPerson.String())
The output is:
➜ ~/Learning/GoTests/overridedefaults go run .
Shirt size: L, color: White. Pants size: 34, color: Blue.
Again this is a completely contrived example, but hopefully it illustrates the pattern well. If you imagine having a Backend object that has interfaces for logging and database fields, they might get assigned default objects that satisfy those interfaces. If we allow the overrides, then a part of our program using a Backend can define its own logging/database type that satisfies the interface, and override the defaults in a very uniform way. If you add to your program later and need different logging/database for that part, this pattern already supports allowing you to override them without changing the Backend object or its support functions.
Running Example Code
While I use macOS/Linux for development and learning, Go is very well supported on Windows and other than making the directory, the example commands below will be the same.
Running example code on a Linux/UNIX/macOS based system:
Create a new directory anywhere you’d like (I do so under a ~/Learning/GoTests/ folder):
mkdir -p ~/Learning/GoTests/overridedefaults
Initialize your directory using go mod init:
cd ~/Learning/GoTests/overridedefaults
go mod init overridedefaults
Create your main.go file:
touch main.go
Copy and paste the code below into main.go and save it. It’s usually a good idea to tidy up your project, so it’ll download any packages you reference that it doesn’t already have:
go mod tidy
Now just run the example with:
go run .
Full code listing:
package main
import (
"fmt"
"github.com/pkg/errors"
)
// Our base object with items we may want to override. Ideally
// this would be defined in its own package
type Person struct {
shirt Shirt
pants Pants
}
// This declares a type that is a func, accepting a pointer
// to a Person struct, and returns a potential error
type Option func(p *Person) error
// Allocate a new instance of our Person struct in a uniform way
func NewPerson(opts ...Option) (*Person, error) {
// All new Person's default to "XL" shirt/pant sizes
// and "Black" for the colors
p := &Person{
shirt: Shirt{
size: "XL",
color: "Black",
},
pants: Pants{
size: "XL",
color: "Black",
},
}
// If they provided any options to override defaults apply them now
// Return an error if any option function returns an error
for _, option := range opts {
if err := option(p); err != nil {
return nil, errors.Wrap(err, "error applying option")
}
}
// Everything went well, return the pointer to their Person object
return p, nil
}
// Implement the fmt.Stringer interface to print out our
// object in a uniform way
func (p *Person) String() string {
return fmt.Sprintf("Shirt size: %s, color: %s. Pants size: %s, color: %s",
p.shirt.size, p.shirt.color, p.pants.size, p.pants.color)
}
type Shirt struct {
size string
color string
}
func OverrideShirtSize(size string) Option {
return func(p *Person) error {
if size == "" {
return errors.New("shirt size cannot be empty")
}
p.shirt.size = size
return nil
}
}
func OverrideShirtColor(color string) Option {
return func(p *Person) error {
if color == "" {
return errors.New("shirt color cannot be empty")
}
p.shirt.color = color
return nil
}
}
type Pants struct {
size string
color string
}
func OverridePantsSize(size string) Option {
return func(p *Person) error {
if size == "" {
return errors.New("pants size cannot be empty")
}
p.pants.size = size
return nil
}
}
func OverridePantsColor(color string) Option {
return func(p *Person) error {
if color == "" {
return errors.New("pants color cannot be empty")
}
p.pants.color = color
return nil
}
}
func main() {
// Create a default Person object/instance
person, err := NewPerson()
if err != nil {
panic("failed to create new Person")
}
fmt.Println(person.String())
// Create an instance with a different shirt size and color
redXXLShirt, err := NewPerson(OverrideShirtSize("XXL"), OverrideShirtColor("Red"))
if err != nil {
panic("failed to create new red XXL shirt Person")
}
fmt.Println(redXXLShirt.String())
// Create an instance with a different pants size and color
whiteMPants, err := NewPerson(OverridePantsSize("M"), OverridePantsColor("White"))
if err != nil {
panic("failed to create new white M pants Person")
}
fmt.Println(whiteMPants.String())
// If we have some options we may want to override consistently,
// we can capture them in a slice, and pass that to our
// NewPerson function
opts := []Option{
OverrideShirtSize("L"),
OverrideShirtColor("White"),
OverridePantsSize("34"),
OverridePantsColor("Blue"),
}
overridenPerson, err := NewPerson(opts...)
if err != nil {
panic("failed to create new overriden Person")
}
fmt.Println(overridenPerson.String())
}
Final Note
As usual nothing is ever perfect. So if anyone finds errors or has ideas to make parts clearer, please feel free to reach out to me altf2o@gmail.com - Thank You!