Code Review – Part 2: Pointer vs Value Semantics

When writing Go code, choosing between pointer and value semantics are extremely consequential design decisions. When reviewing code it is important for us, as reviewers, to recognize this and to look at these decisions with scrutiny. Mixing up pointer and value semantics can lead to subtle bugs, hard to trace race conditions, and unnecessary performance overhead. While Go is praised for its simplicity, pointer vs value semantics is something that I often see as a point of confusion for developers and this guide should provide some practical guidance for when to use what. 

Variables

There are a handful of rules that are helpful for determining when value semantics should be used. It is important to note that these are general rules, like all rules in programming there are situations where they may be broken.

Use value semantics when

  1. Working with built-in types such as ints, strings, bools, floats, etc… that are Go’s version of primitives, you should almost always use value semantics.
  2. Type behaves like a primitive, such as in the case of time.Time where it is an immutable value type i.e. methods do not modify the receiver, they return new time.Time values.
  3. Structs are small and naturally value types with no mutable fields. As with the time.Time example above, this is a case where a new field or struct would be returned rather than a mutated version of the original.
  4. Concurrency safety is a concern; value receivers are inherently safe from data races. This is because each goroutine will get its own copy of the variable.
  5. Useful and usable zero-values need to be leveraged

Use pointer semantics when 

  1. You need to share state across function boundaries. This applies to things like database connection pools, application configurations, and often instances of third party tools such as an email sending package.
  2. Structs contain fields that cannot be safely copied (wait groups, mutexes)
  3. Structs are very large and copying would be expensive. It is important to note that this kind of optimization should generally be in response to performance issues, not in anticipation of them.
  4. Methods need to mutate the state of the receiver rather than get copies.
  5. Absence of value needs to be represented via nil.

Pointer receivers vs value receivers on methods

When it comes to pointer/value semantics on methods, consistency is critical This means that if even one method requires a pointer receiver, all methods on that type should use pointer receivers for consistency. Methods give structs their functional capabilities, and the focus really should be on the data and not the receiver choice. 

Generally we want to use value receivers for methods when

  1. The method does not need to mutate the receiver. 
  2. The receiver type is designed to behave like a primitive.
  3. The struct has no mutable fields or pointers within it.
  4. You want the type to be inherently safe for concurrent use without the need for synchronization.

You should use pointer semantics for method receivers when

  1. The method needs to mutate the state of the receiver.
  2. The struct contains fields that cannot be safely copied.
  3. The struct is large and copying would be expensive. Again, this is not an optimization that should be done up front in general but rather as a response to optimization needs.
  4. Any other method on the type already uses a pointer receiver.

Pointer loop variable trap

This is something to look for Go versions < 1.22. Basically, the loop variable was a single variable that was reassigned for each iteration. Keep an eye out for loops wherein the loop variable is passed into or used in a goroutine. The old fix was to reassign the loop variable before passing in. This has been fixed since Go 1.22, now each loop gets its own copy of the loop variable.

Maps, slices, and interfaces

When declaring a map, the value actually contains a pointer to the underlying hash table structure. When you pass a map to a function, you’re passing the value, which contains this pointer. modifications to map contents inside a function will therefore always affect the original. Practically speaking this means that you almost never will have to pass a pointer to a map. Due to this, the zero-value of map is always nil as well. 

Similarly, slices are structs that contain a pointer to a backing array as well as its length and capacity. Where this differs from maps, though, is when passing a slice, you are passing a copy and not a reference to the original. Modifications to existing elements do modify the original backing array though. When using append(), it may create a new backing array, this is done when the current capacity of the backing array is exceeded. This can lead to some very subtle gotchas and is something that code reviewers should be aware of. 

For interfaces, the main thing to consider in terms of pointer/value semantics is that pointer receivers affect interface satisfaction. A receiver method declared with pointers can only satisfy interfaces when using pointer semantics. If a variable satisfying an interface is declared using value semantics it will cause a compilation error. This restriction exists because some expressions in Go (literals, return values) are not “addressable,” meaning Go won’t let you take their address since they don’t have a stable location in memory.

Conclusion

When reviewing pointer/value semantics it’s important to be consistent across all codebases within an organization and to make sure that developers are on the same page. As with most code reviews, we want to find that balance between premature optimization and making good design decisions the first time. In general, keep in mind that value semantics provide safety and simplicity by default. Reach for pointers when you need mutability and sharing and only use them to optimize after it has been determined that performance is an issue. Stick with these general recommendations and you’ll be set to review these elements in a codebase with clarity and consistency. These patterns will pay dividends and set the tone for great design patterns as your codebase and organization grow.

Share this blog post:

Related Posts