Skip to main content
Preslav Rachev

Photo Credits: davisuko

UPDATE: Not unexpectedly, my post was massively downvoted on Reddit. That’s why I am kindly asking you to give this post a thumbs up on Reddit if you find it the slightest bit worth sharing with the broader Go community. Thank you 🙏

Note: this post has been remotely inspired by Bob Nystrom’s famous What Color is Your Function? 

While my blog post is totally unrelated, I wanted to hint at the idea of having to choose between different types of value and pointer semantics and the eventual lack of code consistency that may arise from that. Keep in mind that this is something that I personally find bothersome, but others in the community find unimportant enough to bear talking about. It will most likely not match your opinion (and that’s OK).

My biggest gripe with pointers in Go (and C before it) has never been what they are or what they are used for. It’s that they are so prevalent that I often ask myself why programming languages like Go are value-first and not reference-first. I know I know - memory, heap, garbage collection pressure. I am aware of all that - after all, I spent most of my programming career writing Java code. I know the pain of running an app that eats a few gigs of RAM from the get-go.

Which is why I like the idea of copying values on the stack. When we talk about primitives, it is utterly indispensable. Java has primitive values too, but in the name of consistency, a large portion of the Java code out there would use their boxed counterparts (Integer, Long, Boolean, etc.). Those are classes wrapping up a primitive value for it to be treated like a reference. You can imagine where those end up being stored - yes, on the heap. If you run a Java profiler, you will notice that much of the heap gets consumed by primitives disguised as object references.

So, treating primitives as pure values is a fantastic idea. It is the more complex user types I am actually concerned about. To clarify my point, I’d limit user types to structs only. This is where the “color” of a type starts showing. And the color boils down to a straightforward question - should I pass this struct by value (copy the value around) or via pointer? This is a simplified version of the more general, “Is this a value type or a reference type? Does it actually matter”?

This kind of judgment may lead to inconsistencies between types in a Go codebase - some are being passed as values, others via pointers. And it is hard to find two developers agreeing to draw a fine line in the sand.

The simple, but IMO, short-sighted rule is to treat everything as a value and pass a pointer to it only when it needs to be modified. This will quickly fall apart once the project gets larger than a few lines of code. Because, as much as I’ve been trying to avoid pointers, they are absolutely ubiquitous. The mutability factor is one of a multitude of mutually exclusive cases:

  • Pass around a handle to a value you want to mutate (or has a method that mutates it)
    • Or one that already contains explicit or implicit (slices, maps, channels, funcs) references as its attributes.
  • Pass around a handle to a value that is too large to copy around
  • Present a graph-like indirection (i.e. when a Person struct has a Spouse attribute with is also a Person. You can only address that via pointer)
  • Present the absence of a value (which is different from a zero-value) via nil
  • Must pass a single reference around (e.g. a database connection)

For example, what does passing a pointer to a single database instance around have to do with mutability? Or passing around a pointer to a struct which is “too big” for that matter? Or when you have a Person-to-Person graph relation, but Person also has an Address attribute. Do you use a pointer semantic for Person only, or that applies to Address too? Whichever decision you go for, how would you justify it to a junior programmer who has just entered the project?

This is the kind of color I am talking about - not being sure you used the right semantic for the given type, potentially causing yourself and others pain working with it in the future. It’s also why I changed my mind in recent years - I am now in the “use pointers for most user types, unless you see a reason to optimize” camp. It’s just more convenient and reduces the level of pointless discussions about the “color” of type A or type B. After all, it was the Go team itself that said “when in doubt, use a pointer,” wasn’t it?

P.S. As mentioned in the beginning, this is something that I personally find bothersome but others in the community find unimportant enough to talk about.

Further Reading #

User or *User - Do We Need Struct Pointers Everywhere?
A bit of up-front thinking can help make our Go code cleaner and more performant.

Have something to say? Join the discussion below 👇

Want to explore instead? Fly with the time capsule 🛸

You may also find these interesting