by Leon Rosenshein

Pointers and Errors

Go is pass by value. But it has pointers. And multiple return values. So here’s a question. Let’s say you’ve got a Builder of some kind, but there are combinations of parameters that are invalid, so your builder returns a struct and an error. Simple enough. The caller just checks the returned error, and if it’s non-nil does whatever is needed. If it is nil, just continue on and use the struct.

But what if the user forgets to check the returned error? Then what? They’ve got a struct that might or might not be usable, but it’s certainly not what they asked for. What’s going to happen if they try to use it? Even worse, what happens if they store it for a while, then pass it down some long call tree, and then it gets used. And fails. Very far from where it was created? That can be a nightmare to debug.

One way to prevent that is by returning a pointer to a struct, instead of a struct itself. Then in the error case you can return a nil pointer and the error. There’s no chance of that nil pointer working. As soon as the code tries to use it you’ll get a panic. Panic’s aren’t good, but they’re often better than something random that almost works. So let’s always do that. Right?

Not so fast, because now you’re using a pointer to things, and suddenly you're exposed to spooky action at a distance. If one of those functions deep in the call tree changes something in the struct via pointer, it’s going to impact everyone who uses the struct from then on. That might or might not be what you want.

Consider this playground. When you create a Stopwatch you get one back, regardless of whether your offset string is valid (sq) or not (s2). So even if you get an error .Elapsed() will return a value. You don’t get a panic, but you do get an answer you weren’t expecting.

On the other hand, if you create a *Stopwatch with a valid offset you get one back (s3), but if your offset is invalid (s4, s5) you don’t get one back. As long as you check your error value (s3, s4) you can do the right thing with a *Stopwatch, but if you don’t (s5), boom - panic.

Finally, consider what happens in the timeIt methods. With the both the Stopwatch (s1) and *Stopwatch (s3) you get the right sub-time, but when you get back to the main function and check the overall time the Stopwatch (s1) gives you the right elapsed time, but the *Stopwatch (s3) has been reset and shows only 1 second of elapsed time.

So what’s the right answer? Like everything else, it depends. Don’t be dogmatic. I lean towards pointer returns because it’s harder to use an invalid one, and not pointers for method parameters unless I want to mutate the object. But that can change based on use case and efficiency. What doesn’t change is the need to think about the actual use case and make an informed decision.