Understanding Pointers in Go: Beyond the Basics

Pointers can be one of the more nuanced features of Go, I would say. While they’re not as complex as some languages memory management tools, Go's simplicity around pointers makes it essential to know when, where and how to use them. In this post, we’ll look at the ‘address-of’ operator (&), the new function and a neat way to work with pointers using generics, while focusing on practical scenarios where each approach makes sense.

The Basics: Address-of Operator (&)

The simplest way to create a pointer is to use Go's address-of operator, &. It allows you to refer to the memory address of an existing value. This is useful when you want to share data across functions or when you need to manipulate large structures without copying them. Here’s a quick example:

value := 42
ptr := &value

In this case, ptr holds the address of value. You can now pass this pointer to other functions or mutate the value without copying it around. This is especially useful when dealing with structs or slices where the performance matters.

When to use &:

  • You need to pass around references to large structures or data without copying.

  • You want to modify the original value from different parts of your code.

  • You need simple, efficient reference sharing within your program.


Allocating New Pointers with new

The new function is a built-in Go feature that allocates memory and returns a pointer to the zero value of the type you specify. Unlike the address-of operator, new is useful when you want to ensure memory allocation for a specific type but don’t have an existing value to reference.

ptr := new(int)  // Allocates memory for an int
*ptr = 42        // Assigns a value to the allocated memory

This method of pointer creation is less common in Go than in some other languages and for good reason. The memory allocated by new starts out as the zero value for the type, so in most cases, you’ll end up setting the value later. It's worth noting that using new can sometimes be overkill for simple scenarios where you already have a value on hand. Go encourages simplicity and sometimes, the address-of operator is all you need.

When to use new:

  • You need a pointer to a value but don’t have an initial value available.

  • You want to allocate memory directly but without initializing it immediately.

  • When you require a pointer as part of a larger composite type but don't want to manage the value upfront.

But beware of using new unnecessarily. In idiomatic Go code, you rarely see new used unless you're dealing with specific scenarios where the pre-allocation is critical.


A “Generic” Approach

With Go 1.18's introduction of generics, you can now write a function that returns a pointer to any type ‘T’. This allows for greater flexibility and reusability across your codebase, especially in situations where you need pointers for various types without duplicating the logic.

func Ptr[T any](t T) *T {
    return &t
}

This function returns the address of the value passed to it and thanks to Go's efficient memory model, it does this without unnecessary allocation. One key detail here is that t is still scoped to the function. If you're returning a pointer to t, you need to ensure that it lives beyond the function’s scope, or you could end up with a pointer to invalid memory.

When to use a generic pointer function:

  • You want a flexible way to create pointers for different types without code duplication.

  • You’re dealing with a larger codebase that could benefit from generic utility functions.

  • You’re in a performance-critical situation where minimizing allocations matters.

Caveat: Be cautious about using this approach when the pointer is intended to outlive the function call. If the function ends and you’re holding a pointer to a variable that no longer exists, you’ll end up in unpredictable territory, as the memory could be garbage collected or repurposed. Go’s garbage collector is robust, but it's not a free pass to ignore lifecycle management.


Deciding When to Use Pointers

Go encourages clarity and simplicity and pointers are no exception. In most cases, Go developers prefer passing values directly, especially small or simple types. But there are times when pointers make more sense:

  1. For large structs or data: When passing around large data structures, pointers are more efficient because they avoid copying the entire object.

  2. For modifying data in place: If you want a function to update the caller's data, you'll need to pass a pointer so the function can modify the original value.

  3. For reference sharing: Sometimes, you just need different parts of your program to share the same reference to a value, especially when working with stateful data.

On the other hand, pointers aren't always the best choice. Avoid them when:

  • You’re dealing with small, simple data types that don't need to be shared or modified in multiple places.

  • You want to keep things simple and avoid the complexity that can come with managing memory and lifecycles.


Wrapping Up …

Go provides simple yet powerful tools for working with pointers but the choice of approach should always depend on the situation. Like everything in Go, the focus should be on writing clean, maintainable and efficient code.

Happy idiomatic coding! :)

Thanks to Anthony GG and Go community, for inspiring me to write this post.

0
Subscribe to my newsletter

Read articles from Ashwin Gopalsamy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ashwin Gopalsamy
Ashwin Gopalsamy

Product-first engineer, blogger and open-source contributor with around 4 years of experience in software development, cloud-native architecture and distributed systems. I build fintech products that process millions of transactions daily and drive substantial revenue. My expertise spans designing, architecting and deploying scalable software, focusing on the business under the code. I collaborate closely with engineers, product owners, and guilds, known for my clear communication and team-centric approach in dynamic environments. Colleagues appreciate my adaptability, openness and focus on diverse, meaningful contributions. Beyond coding, I’m recognized for my documentation, ownership and presentation skills, which drive clarity and engagement across teams. Bilingual in English and Deustch, I bridge cross-functional teams across geographies, ensuring smooth, efficient communication. I’m always open to new opportunities for connection and collaboration. Let’s connect and explore ways to create together.