Last Updated: 2021-07-20

In this guide we seek to discuss lessons learned regarding return values.

Introduction

We often have methods which may or may not return a value. For example, a Person may have a method FavoriteCar(). Or we may have a function such as ParseAuthToken(raw string), FindTagValue(tag string), ParseRequest(req []byte).

In the above examples, the happy path should involve returning the result. A Car struct{}, an AuthToken , a string Tag Value, etc. But how do we indicate when a Person has no favorite car? Or the auth token wasn't valid to parse? Etc?

Using pointers

It may be tempting to return a *Car. Or a *AuthToken, *string, etc. In this pattern we are effectively saying that a *struct{} is a nullable type and the receiver must perform a nil check.

This ultimately leads to sloppy code as nil checks become sprinkled throughout the code and the developer loses a sense of where they are needed and where they may have been missed. This pattern often leads to these nullable types being passed in as parameters which furthers the complexity of error handling and often leaves the developer clueless as to the original source and reason for the nil.

Nullable types are simply not first class citizens of golang and not the golang way. Attempting to them often leads to failure.

Note: This does not mean it is inappropriate to return pointers. It may be appropriate to return a struct{} or *struct{} based on other requirements. It is simply to say that if you use *struct{}, you should provide an additional mechanism beyond a nil check to test the validity of the returned value.

Using errors

If our function is sufficiently complex and may fail for various reasons, returning an error value may be appropriate. For example, if Person.FavoriteCar() involves external calls (Database, RPC, etc.) we provide more visibility (for logging) and ability to handle errors (perhaps we will retry) with an error.

But what about map[]? When we attempt to find a non-existent key in map[], it simply returns an ok bool.

Using ok

For simpler methods, it often makes sense to return an ok bool. This is similar to how map works.

itos := map[int]string{1: "one", 2: "two"}

s, ok := itos[1]
fmt.Println(ok, s) // true one

s, ok = itos[2]
fmt.Println(ok, s) // true two

s, ok = itos[3]
fmt.Println(ok, s) // false

Playground

GoFlagship usage of a Zero struct{} for the error case