I share my passion for technology and products. Senior Engineer @intercom.
01 April 2019
Pointers are a great way of building software in a more efficient and less memory intensive fashion. I find however, there is a great source of confusion and misuse around their application that lead to less intuitive and often bug prone programs.
Let’s see a typical usage of pointers:
So, what’s the issue with the above code anyway?
Mutation is difficult to tackle when things go wrong. A million things could have altered the state of your program. Now, it’s not to say mutation is necessarily bad but in a lot of cases we can improve its visibility. For instance, instead of having transformName
doing the mutation, have it return a string
. This will force the propagation of the mutation to happen at a more visible place.
The above though doesn’t really feel like idiomatic Go. Moreover, there is a better way of achieving a similar effect to the above.
Methods are just functions that have a particular receiver. Attaching behaviour that is operating specifically on a type is the perfect usecase. It also makes code much cleaner.
The above has also the positive side-effect that by scanning the methods of a type, you can see where the mutation happens instead of having to pass pointers around in your application.
Type driven development is a way of developing software specifically in pure functional languages like Haskell and Elm. When reading the type signature of a function it should be very clear to the reader what this function will do. Let’s have a look at the signature of transformName
Hm.. So it takes a pointer to a Human
and returns nothing. There are a lot of questions that are raised based on this signature. Why does it take a whole Human
to operate just on its name? 🤔 Is there a possibility that it could also operate on its surname? Does it have different rules based on the Human
’s locale?
Let’s look at the solutions we mention above.
In the case of the function that returns just a string, the type signature is simply
Nice! It’s very clear to the reader that this function receives a string and returns a string. 🆒
What about the method? Well that’s an interesting one. On the surface, it’s no better than the initial implementation. I beg to disagree though. By hiding the internal implementation and attaching it to the Human
type, we have now given full control of details to the struct itself.
Purity is a term you normally associate with functional languanges such as Haskell but that doesn’t mean we cannot make use of it in Go as well. Functions without side-effects makes refactoring so much easier. Let’s say we are staring at the initial solution fresh. Practically, that function takes a string and uppercases it. So why does it need to be associated with a Human
and it’s Name
? It sounds like it could live under the string
package. Indeed such a function does exist! But let’s assume that it didn’t exist. What would our code look like?
Great. But we can make it more Go-like based on what we have discussed so far.
Whether is worth the extra step or not is up to the programmer and the circumstances.
Up until this point, I mentioned that pointers are mostly an optimization strategy which is slightly misleading. In his book Go in Action, Bill Kennedy goes in great detail about pointers semantics. Pointer semantics are the way we should think about the way we build software in Go. Performance can come later.
Pointer semantics allude to the idea that certain entities in your software will need to propagate any changes to the rest of the program. We see this a lot with configuration and structs that represent table data. Let’s say we have a User table and we have a User
struct type. Does a change in the User
’s email need to be realised by another part of the program that is operating on that entity? If this answer is yes, mostly likely you need to be using a pointer to represent that entity.
Having a function receiving or returning a pointer, means that function can receive or return nil
. I find this pattern to be code smell. Let’s look at an example.
At first glance, this makes sense. However, we have used the fact that findUser
returns a pointer to sneak in a nil
value. This is bad in my opinion for two reasons.
In languages like Ruby where types are not a thing, nil
is pretty much the only way to represent the absence of a value. But with Go we can do so much better.
This approach, builds on the whole idea of Go’s multiple returns with the last one being an error
. Now our code is super clear. Having findUser
being responsible knowning about errors also helps to customize the type of errors that we could have and have the caller act accordingly.
Obviously working with pointers forces you to think and structure your application in a different way. I argue that it shouldn’t really matter the way we design our interactions between our functions. Let’s flip it on its head a bit and see our functions as if it was returning a value instead of a pointer.
In this case we cannot just return nil
which would have forced us to use one of the ways we experimented with earlier. Naturally, and given practical requirements, we would have opted in for returning an error
as second argument. The convenience of being able to return nil
however, clouded the way we should have looked at that piece of code in the first place.
To recap: pointer semantics should be the driving force when using pointers. Performance can come later. Avoid passing nil
when pointers are expected.