Skip to main content
Preslav Rachev
  1. My Writings/

User or *User - Do We Need Struct Pointers Everywhere?

·8 mins

Cover photo credits: Stable Diffusion

Let me preface this blog post by saying that everything you ever read on this topic should be considered highly subjective. This presents my personal point of view, and while helpful, it may not be 100% for every case. Always benchmark.

Coming from Java to Go a few years ago, I could have just fallen into the comfort of decorating all my types with a * and called it a day. And yet, something kept bugging me. This was not the land of Java, where everything is a heap-allocated pointer in disguise. The designers of Go must have kept pointers explicit to make the developer think about when it is appropriate to use them, not just wave them around like a Golden Hammer.

Indeed, when it comes to tips and community advice, we have agreed that the more we can put on the stack (respectively, the less on the heap), the better. But that’s not how Go code out there looks like. By and large, pointers are being used exclusively - everywhere and for everything. While this¬†“no time to think, need to get the job done” ¬†approach will work in every situation, I believe we can improve our software by drawing a simple line. That’s what this post is all about.

This is not a tutorial about what pointers are or how to use them. There are many great resources on this topic, which I advise you to look at if you are unfamiliar with the subject matter.

Structs are primitive by nature #

Here is a dilemma I’ve been musing around for quite a while. Go, like its spiritual predecessor, C has structs. By its nature, a struct is nothing more than a grouping around several variables. Say, a Config struct might have a RootFilePath, an EmailServiceToken and a Timout attribute - all plain strings. In other words, a struct name-spaces those variables so that we can have multiple names and ages and still easily distinguish between them.

type Config struct {
	RootFilePath string
	EmailServiceToken string
	Timeout time.Time
}

conf1 := Config{
	RootFilePath: "/tmp/conf1", EmailServiceToken: "abcd", Timeout: time.Second*5
}
conf2 := Config{RootFilePath: "/timp/conf2", EmailServiceToken: "efgh"}

A struct, like other Go primitives, is a value type by default. It is designed to work like a primitive by default. If a function receives a Config value as one of its parameters or returns a Config value as its result, the value will be copied, meaning the values of all of its attributes will be copied.

This is the kind of thing that might scare people at first. Some might be like - well, copying an entire struct is probably more expensive than just passing a pointer to it. First,¬†passing¬†a pointer means¬†copying¬†it around ‚ÄĒ and pointers have their size, too (8 bytes on a 64-bit machine). Second,¬†de-referencing¬†a pointer also comes with a cost, primarily when those pointers are located somewhere on the heap. In that case, we hit the third thing: the garbage collector must clean heap-allocated memory. So, fewer heap-allocated pointers, less work for the GC.

For what it is worth, you can share conf1 and conf2 without ever using a pointer to any of them and still be okay. Currently, they are simply bags (or containers) for primitive data.

Plus, when I say that not every struct type in your Go code should be a pointer type, it does not mean you can’t pass a pointer to your type around when you need to. In fact, I recommend that people¬†always prefer pointer semantic¬†when adding methods to a Go type. It will make the entire codebase consistent, and in most cases, it won’t cause extra heap allocation.

Why use struct pointer types, then? #

If only life could be that simple. I have written software long enough to know there is no single silver bullet solution. Some technologies claim that they have found it, presenting the developer with one way of writing code while performing all kinds of quirky dark magic under the hood. Go is on the other end of the spectrum - being annoyingly transparent about every single bit of your program that could go wrong.

Regardless of the developer paradigm (OOP, FP, procedural, etc.), most software deals with some form of¬†objects¬†that get thrown around and functions that operate on those objects. Thus, for the rest of the discussion, when I refer to something as an object, I will mean an instance of some custom type with its own properties and, possibly, methods. Please don’t take this as me calling Go an OOP language or something.

In every piece of software I have encountered, I have seen three types of objects:

Short-lived value objects

These are many but small and temporary in nature. Think of that Config struct we used above. You use it once to set things up and are free to throw it away. Another great example is all the sorts of data transfer objects we use when reading data from a database, returning a JSON response, or passing it to a template. All those are essentially the same thing - a bag of properties you fill in, use, and throw right away. Unless the same data gets requested again immediately, there is no real need to keep it around for long after it has served its purpose.

To answer the question from the title. If User is a struct we use to read data from the users table in the database, we might as well name it UserRow and treat its instances like value objects all over. There is no need to allocate them on the heap; just let the runtime copy them back and forth.

Long-lived, single-reference “service” objects

When I think of a reference object, it is usually something you want to keep around so that you can refer to it from different places. Those are (not always, but often) ones that are more method-heavy (e.g., offer the user a way to execute some logic) and contain references to other such objects.

The ideal example of this is your *DB connection - you want a single instance of it that is shared across your entire app, and it presents options for interacting with the database. Another great example might be a *MailSender or a *StripeClient or a, even the ever-present *App struct that usually ends up being a single point of reference to all the other service objects in our projects.

As you may have noticed, I am intentionally adding the * to all of those to indicate that we should consider them pointer types in most cases.

Value-like “models” that represent a graph structure

This group is tricky because it is not so easy to distinguish it from the value object group. These value objects need to be treated as pointer types by necessity. They are also why people in many projects opt to use pointer types for everything.

A pure value object like our UserRow is simple, containing a flat, final number of fields (name, age, etc.). Suppose we have a similarly flat PurchaseRow struct containing a purchase made by a user. So far, so good. What if we now wanted to combine both into a User object with its own slice of Purchase-s, each referring back to the User who bought them? We cannot do that without introducing a reference somewhere. The mere fact that our User contains a slice of Purchase instances means that we already have that reference. Thus, for consistency and simplicity of code comprehension, I would always consider treating more complex model objects as pointer types.

Pointers and optionality #

Don’t justify using a pointer just to express a form of “optionality” - when a value may or may not be there. While it certainly can be used for that purpose, and a large majority of Go codebases use pointers for that purpose, it can be quite a foot gun. Not only does it require nil checks, but it often collides with the zero value of the type. Check this example:

type Address struct {
	StreetName string
	Country string
}

type User struct {
	Address *Address // do we really gain anything from that pointer here?
}

If we initialize a user without an address, the value of the address will be nil. Fine. Then what about the following:

u := User{Address: &Address{}}

The address is there, but is it valid? Nope. Now, you will have to do even more checks. I hope you are getting the point.

Can’t we just treat all structs as pointer types? #

As I mentioned above, you could, but you are effectively taking away all the advantages of simple value types. Keeping in mind that C# has those, and they might find their way into Java as well, means that there is a point for distinguishing between what goes to the heap vs. what could be discarded right away.

Before you go #

I just want to repeat what I said at this post’s beginning. These are pure heuristics based on my work with Go, and they may not necessarily represent your views or your project. Moreover, while we generally know what piece of data goes in which part of memory, this is not always the case. In many situations, the compiler may put a struct value on the heap simply because it is too big. In other situations, it might inline a function into another, effectively removing the need for a pointer heap allocation. At the end of the day, we never know, so it is always best to do some thorough benchmarking and decide what action to take.

Reference Material #

Using Pointers In Go
Our Blog is a great source of insights about Go, also referred as "golang". Our main contributor Bill Kennedy is a renowned Go Trainer, consultant, and author.

Have something to say? Join the discussion below ūüĎá

Want to explore instead? Fly with the time capsule ūüõł