Optional is a type of data that can handle a true absence. For example, in a survey form, we may want to distinguish between agree, disagree, or no answer. If we represent such data using a boolean, we should differentiate between false and null values. In this article, we will discuss common patterns of optional impelementation in Go.

Use Cases

Perhaps the most common use case for optional is an API, whether it is for fetching data from the database (e.g sql.Scanner.Scan) or processing data from client’s POST request (e.g json.Marshal or json.Unmarshal)

Unlike Java or other languages, Go doesn’t have a straightforward optional type. Instead, developers rely on either nil (along with null checking) or an extra boolean to check absence.

Approach 1: Pointer

This is the simplest approach because all we need is initializing a pointer which can hold a nil.

package main

import "fmt"

type CreditAccount struct {
	HolderName string
	Score      *int
}

func NewCreditAccount(name string) *CreditAccount {
	newAccount := CreditAccount{}
	newAccount.HolderName = name
	return &newAccount
}

func main() {
	fmt.Println(NewCreditAccount("Alice").Score)
}

Because score is a pointer to int, it defaults to nil instead of zero value 0 if not defined.

This approach can be applied to any data type, such as bool, int, string, or any struct.

One thing to note about this approach is that nil checking is important to avoid panic before dereferencing or exposing a pointer’s value. If not done carefully, this can lead to problems (for example on a web server).

Approach 2: Presence Flag

Given a simple struct as an example

type OptionalInt struct {
	Value int
	Valid bool
}

We can check for the equality of two variables as follows.

package main

import "fmt"

type OptionalInt struct {
	Value int
	Valid bool
}

func main() {
	foo := OptionalInt{Value: 0, Valid: true}
	bar := OptionalInt{}

	if foo.Valid == bar.Valid && foo.Value == bar.Value {
		fmt.Println("Equal")
		return
	}

	fmt.Println("Not Equal")
}

Although bar.Value returns the same zero value 0 as foo, it is meant to be different.

This approach is somehow idiomatic for basic data types that the sql package has a lot of wrapper types such as sql.NullString, sql.NullInt32, and so on. Go also has a nice feature to represent this kind of wrapper.

Let’s make a “getter” for our “OptionalInt” type…

func (o OptionalInt) Get() (int, bool) {
	return o.Value, o.Valid
}

…and it’s caller.

func main() {
    foo := OptionalInt{Value: 0, Valid: true}

    if val, ok := foo.Get(); ok {
        fmt.Println(val)
    }

    // ...
}

With this approach, we won’t encounter nil panic but we still can check for absence.

Approach 3: Generics

Since generics was introduced in Golang, a handful of data structures and generic functions can be implemented in a breeze. One of the most interesting uses of Go generics is an optional type.

For example, we can refactor our OptionalInt type into a generic type…

type Optional[T any] struct {
	Value T
	Valid bool
}

func (o Optional[T]) Get() (T, bool) {
	return o.Value, o.Valid
}

…and instantiate it.

func main() {
    foo := Optional[int]{Value: 0, Valid: true}
	bar := Optional[string]{Value: "asd", Valid: true}

	if val, ok := foo.Get(); ok {
		fmt.Println(val)
	}

    // ...
}

Some Go packages such as mo.Option even implement the interfaces related to optional use case such as sql.Scanner.Scan and json.Marshal, so we don’t need to construct the type manually.

Best Practices

Absence of complex structs is usually represented by nil and delivered along with an error or boolean flag.

// func FetchObject() (*Object, error)

obj, err := FetchObject()
if err != nil {
	fmt.Println("Error: ", err)
	return
}

fmt.Println(obj)
}

It is good practice to return an error when a function returns an absent or nil value, so it doesn’t confuse the user.

For basic data types like int or bool, distinguishing zero/false from nil can be tricky. In my opinion, all approaches we discussed above are fine, but I like the generics approach more although it requires a recent version of Go.

Lastly, there are widely accepted practice to just omit absence whenever possible. That means that we leave the absent value as blank/zero, insert that blank value into the database, and return it as null when fetched. To make the last thing happen, we can use struct tags.

For instance, we can have a name struct with optional middle and last names as follows.

type Name struct {
	First  string `json:"first"`
	Middle string `json:"middle,omitempty"`
	Last   string `json:"last,omitempty"`
}

If we create a Name of John Doe (no middle name), we can return a json with no middle name as follows.

package main

import (
	"encoding/json"
	"fmt"
)

type Name struct {
	First  string `json:"first"`
	Middle string `json:"middle,omitempty"`
	Last   string `json:"last,omitempty"`
}

func main() {
	a := Name{
		First: "John",
		Last:  "Doe",
	}

	out, _ := json.Marshal(&a)

	fmt.Println(string(out))
}

This program returns {"first":"John","last":"Doe"}, omitting the middle name column. That means we can return no absent data to users although it is not explicitly defined as absent in our codebase.

In a bigger struct, we may want to not expose all the attributes to reduce complexity. Therefore, we can implement an WithOptions pattern to make our constructor simple yet customizable.