One of the advantages of Go is that null (nil) values are rarer than in some other languages. The following snippet will fail:

func getDay() string {
    return nil
}

In order to make that snippet work, a pointer is needed as null values are the zero value for pointers (the same applies for interfaces, channels and function types).

func getDay() *string {
    return nil
}

Being that pointers can introduce null and hence panics, and knowing that null is considered to be “The billion dollar mistake”, why is still so common in Go to use pointers for everything? Below are some arguments that I faced:

Optimization

The most common example here are slices. There is a general misconception on how much is a lot, in other words, how many items in a slice are too much to copy it by value. Of course, the answer is it depends. And this is true, a slice with 100 integers is probably less memory intensive that a slice with 50 complex structures with nested slices.

That being said, there is a good saying: avoid premature optimization. Favor copying slices by value instead of using pointers unless you have a super specific case that proves the contrary.

You can find here a nice article doing benchmark on slices using pointers and values.

Modifying the receiver

Imagine the following func:

func (cart *Cart) AddItem (item item) {
    cart.Items = append(cart.Items, item)
}

Do you like what you see? Maybe you won’t after visiting these concepts:

Pure functions: A function whose return values are identical for identical arguments - also called deterministic. There is no usage of static variables, mutable references or streams. These functions have no side effects. These kind of functions are very easy to test.

Immutability: An immutable is one whose state cannot be changed after the creation. This means that the immutable is thread safe as you can pass it around contexts knowing they won’t be modified.

Knowing the benefits one can refactor this to eliminate the pointers in something like:

func (cart Cart) AddItem (item item) Cart {
    items := append(cart.Items, item)
    return newCart(items)
}

JSON property representing three different states

Let’s image that for some reason (which I cannot recommend) there is a Person structure that contains a Salary property. The system you are consuming will return the person with different salary depending the employment situation:

  • { "name": "Pablo", "salary": 100 } means paid job.
  • { "name": "Pablo", "salary": 0 } means unpaid internship.
  • { "name": "Pablo", "salary": null } means unemployment.

If this structure would be deserialized with the following struct, can you image what is going to happen?

type Person struct {
    Name   string `json:"name"`
    Salary int    `json:"salary"`
}

If you guessed that Salary will never be nil then you guessed correctly. As explained on the first snippet in this post, the zero value for int is 0. Then as 0 was used for unpaid internship, one would never know the difference between this and being unemployed.

But if the Salary is changed from int to *int, will this work?

type Person struct {
    Name   string `json:"name"`
    Salary *int   `json:"salary"`
}

And the answer is yes, but at what cost? Do we want to introduce deliberately pointers in our system to solve this?

Introducing Maybe library

Functional languages have a lot of interesting features that are having more traction as time passes and end up being ported to other languages. One of my favorites is the Option data type. Think of it as Schrodinger’s cat variable. It may have a value or it may not have a value; and the only way to know it is checking if effectively has one.

UPDATE: Since Go 1.18 has been released, the library now supports generics so it is not limited to string, int, float, time, bool.

Let’s image the following struct:

package maybe

type Maybe[T any] struct {
	value    T
	hasValue bool
}

func Set[T any](value T) Maybe[T] {
	return Maybe[T]{
		value:    value,
		hasValue: true,
	}
}

func (m Maybe[T]) HasValue() bool {
	return m.hasValue
}

func (m Maybe[T]) Value() T {
	return m.value
}

This is how an option implementation can look in Go 1.18. By doing so, null values are not possible. As stated before, while I cannot recommend designing a system where null and 0 represent different things, we can take another look at the previous example:

  • Salary 100 -> Maybe.HasValue() will be true and Maybe.Value() will be 100.
  • Salary 0 -> Maybe.HasValue() will be true and Maybe.Value() will be 0.
  • Salary null -> Maybe.HasValue() will be false and Maybe.Value() will be 0.

This is how it looks like in our previous struct:

type Person struct {
    Name   string     		`json:"name"`
    Salary maybe.Maybe[int] `json:"salary"`
}

But Pablo, this is serialized in the following way, right?

{
   "name":"Pablo",
   "salary":{
      "hasValue":true,
      "salary":100
   }
}

Not really! Go allows customization of the MarshalJSON() and UnmarshalJSON() of a struct, so the serialization will look like:

package maybe

func (m Maybe[T]) MarshalJSON() ([]byte, error) {
	var t *T

	if m.hasValue {
		t = &m.value
	}

	return json.Marshal(t)
}

If the maybe.Maybe[int] was not initialized, then the serialization in our example will look like { "salary": null }; however if maybe.Set(100) was called, then the hasValue will be true and the serialization will be { "salary": 100 }.

Now for the unmarshal, the usage of pointers and null are being handled by the library:

func (m *Maybe[T]) UnmarshalJSON(data []byte) error {
	var t *T
	if err := json.Unmarshal(data, &t); err != nil {
		return err
	}

	if t != nil {
		*m = Set(*t)
	}

	return nil
}

If the json is { "salary": 100 } then the pointer is not null and the maybe.Maybe[int] will be set to HasValue() = true and Value() = 100. In the case of { "salary": null }, the pointer will be nil and therefore the maybe.Maybe[int] will contain HasValue() = false and Value() = 0.

With this implementation we can code the following assumption:

func (p Person) IsEmployed() bool {
    // If salary has value, but it is 0 we assume unpaid internship
    return p.Salary.HasValue()
}

Summary

  • Don’t use pointers unless you really know what you are doing.
  • Try to eliminate the need of nil whenever is possible.
  • Option types are a really nice feature of functional languages.
  • 👉 Link to the library. 👈