Pointers have numerous benefits and to imagine coding without them, let’s say it’s not so pleasant. But they do have cons, one of them (at least for me) might be that they can cause confusion and may even break the code if not used with caution. I think we’ve all experienced when things just did not work as intended, and after a debugging session found out that a pointer caused the unexpected behavior. But after all making a decision about whether or not to pass (or in general, use) a pointer is not so easy for me. So, I decided to study and learn from people much smarter and experienced than me. Hopefully sharing these findings might come interesting to you as well.
This article is heavily inspired by **Hossein Nazari* in their invaluable article titled “Whether or not to use pointers while passing structs”, published on Gocast (FYI: it’s in Persian).*
One of the main deductions that the original author made was:
Set your default to not using pointers, unless you know for a fact that its fields are going to change.
Passing by “value” vs “reference”
- Go passes everything by value. Even pointers get copied when passing them to a function, yet the copy still represents the same underlying memory and so we are able to change the underlying value.
First, let’s take a look at some details revolving around pointers. An interesting article emphasizes on the fact that go is pass-by-value but there are certain times when that doesn’t sound like the case. There are primitive types, e.g. int, string, byte, rune, bool that are considered “value” types whereas pointer types are “reference” ones. But there are certain types that have somewhat of a different nature, as the article puts:
However map, slice and channel are all special types that *are *or *contain *references.
But the interesting thing about these types is that they all point to some other underlying data and even if you pass them (seemingly) by value in an arbitrary function, changing their content in the function will change their original value. To give an example:
package main
import "fmt"
func changeKey(m map[string]string) {
m["key"] = "new_value"
}
func main() {
m := map[string]string{"key": "value"}
fmt.Println(m) // map[key: value]
changeKey(m)
fmt.Println(m) // map[key: new_value]
}
So it seems like the maps are references, though in fact they’re not. Take a look at this example:
package main
import "fmt"
func new(m map[string]string) {
m = make(map[string]string)
}
func main() {
var m map[string]string
new(m)
fmt.Println(m == nil) // true
}
If the map m was a C++ style reference variable, the m declared in main and the m declared in newwould occupy the same storage location in memory. But, because the assignment to m inside newhas no effect on the value of m in main, we can see that maps are not reference variables.
A map reference is a pointer to a map header struct (of type hmap) in memory which contains various fields, including the number of cells and some other things.
Copying a map or passing it as a value doesn’t actually copy the underlying hashmap — it merely copies the pointer to the header struct. Therefore, copying a map reference is just giving you multiple references to the same underlying map in memory. It is the map reference that is being passed-by-value, not the contents of the map
The final result is depicted here as well:
First, Go technically has only pass-by-value. When passing a pointer to an object, you’re passing a pointer by value, not passing an object by reference. The difference is subtle but occasionally relevant. For example, you can overwrite the pointer value which has no impact on the caller, as opposed to dereferencing it and overwriting the memory it points to.
Performance
One of the major concerns while using pointers is performance. And it seems that using a pointer as a way to improve the overall performance and memory efficiency of a piece of code is not always correct.
Backed up with proper benchmarking, Mario Macías suggests an interesting point about returning structs vs pointers:
Despite copying a struct with several fields is slower than copying a pointer to the same struct, returning a struct value may be faster than returning a pointer if we consider escape analysis particularities.
Returning structs allows the compiler to detect that the created data does not escape the function scope and decides to allocate the memory in the stack, where the allocation and deallocation is very cheap if compared to managing memory in the heap.
Also this article points out that in order to justify sharing a pointer to a struct it should be particularly big (how big you might ask? I’m not sure, maybe a few hundred bytes?) and its life-cycle should be reasonably large. And there is another benchmark suggesting that you rarely need to share a struct by its pointer.
It turns out that only extremely large structs can cause slower performance compared to pointers.
If I choose to use a pointer, the variable itself should hold a valuable purpose for that entire duration.
With heap allocations the garbage collector causes brief pauses form time to time (for a couple of milliseconds on average) to clean that up. Since we’re talking about stack and heap, it’s worth noting that accessing the stack is particularly faster, as describe here:
The stack is faster because the access pattern makes it trivial to allocate and deallocate memory from it (a pointer/integer is simply incremented or decremented), while the heap has much more complex bookkeeping involved in an allocation or deallocation. Also, each byte in the stack tends to be reused very frequently which means it tends to be mapped to the processor’s cache, making it very fast. Another performance hit for the heap is that the heap, being mostly a global resource, typically has to be multi-threading safe, i.e. each allocation and deallocation needs to be — typically — synchronized with “all” other heap accesses in the program.
So how does this concern us? Well, it turns out that what we do with return values or input parameters of a function (i.e. deciding whether or not to use a pointer) may result in allocations. This process is thoroughly and beautifully describe by James Kirk in their article:
A general rule we can infer from this is that sharing pointers up the stack results in allocations, whereas sharing points down the stack doesn’t.
We’re going to see more about stack and heap in the following section.
At least for me, one of the major things that I’ve seen many times is that if a team is wishing to evolve and improve their many services, projects and overall code base overtime, it’s almost crucial to prioritize the readability of the code. Writing something that is idiomatic and can be reasoned about fairly easily seems to be a more successful approach rather than trying to achieve even the tiniest optimizations at the cost of the code becoming an unreadable mess. In Golang, generally writing idiomatic and readable code seems to be of great importance and value. William Kennedy described this idea of pointers and probable performance trade-offs in this article:
I am asked quite a bit about when and when not to use pointers in Go. The problem most people have, is that they try to make this decision based on what they think the performance trade-off will be. Hence the problem, don’t make coding decisions based on unfounded thoughts you may have about performance. Make coding decisions based on the code being idiomatic, simple, readable and reasonable.
He later adds that in general if we are to group implementation as either a “primitive data value” or a “to be changed” it’s wise to share the changing one with a pointer:
If you review more code from the standard library, you will see how struct types are either implemented as a primitive data value like the built-in types or implemented as a value that needs to be shared with a pointer and never copied. The factory functions for a given struct type will give you a great clue as to how the type is implemented.
In general, share struct type values with a pointer unless the struct type has been implemented to behave like a primitive data value.
And also:
If you are still not sure, this is another way to think about. Think of every struct as having a nature. If the nature of the struct is something that should not be changed, like a time, a color or a coordinate, then implement the struct as a primitive data value. If the nature of the struct is something that can be changed, even if it never is in your program, it is not a primitive data value and should be implemented to be shared with a pointer. Don’t create structs that have a duality of nature.
By the way I’m not really sure if I can fully understand the very last sentence. “duality of nature” doesn’t seem like a well defined thing to me. To be honest most of the structs with a “changing nature” that I’ve seen so far kind of had their own consistent and unchanging inner state as well. To get you an example of that, consider a struct explaining a vehicle on its way from an origin to a destination (my apologies if this example turned out to be kind of half-baked and not so true, it’s just an example):
package main
type Point struct {
Lat float64
Long float64
}
type Vehicle struct {
id string
Origin Point
Destination Point
GasolineLeft float64
}
I’m not saying that he’s wrong, not at all, all I’m saying is that this duality of nature is something that might be observed here and there, and that’s fine (you can argue that the design above can be altered in a way that maybe origin and destination are stored somewhere along with the id or something, but, you get the point). My own take on that article is to use a pointer for the structs that are going to change even slightly.
And the reference types (e.g. map, slice, chan, interface and function values) happen to be shared with a pointer only in rare scenarios, and if you’re not sure, just don’t use a pointer.
Reference types are slices, maps, channels, interface and function values. These are values that contain a header value that references an underlying data structure via a pointer and other meta-data. We rarely share reference type values with a pointer because the header value is designed to be copied. The header value already contains a pointer which is sharing the underlying data structure for us by default.
If you review more code from the standard library, you will see how values from reference types in most cases are not shared with a pointer. Since the reference type contains a header value whose purpose is to share an underlying data structure, sharing these values with a pointer is unnecessary. There is already a pointer in use.
In general, don’t share reference type values with a pointer unless you are implementing an unmarshal type of functionality.
Stack vs Heap
We’ve seen that in general sharing the pointers up the stack results in heap memory allocations. Let’s dive deeper into that.
This section is inspired by Jacob Walker, in their video “The Stack and the Heap — GopherCon SG 2019” publish on YouTube.
There are two kinds of memories, Stack and Heap, and with go, we have multiple stacks (i.e. a stack for each goroutine), and a heap which basically houses everything else that is not in the stacks.
While we can’t really tell by ourselves if a variable is going to be on the stack or on the heap, the main question is “Does it really matter?”.
For the correctness of a program, NO. But it does effect the performance of a program, because everything on the heap is managed by the garbage collector, which causes some latency for the whole program.
First of all, let’s make sure that we are not excessively spending time on over engineering and optimizing our program while we see no real need nor a benchmark proving a poor performance. In other words, if the program is “fast enough”, just let it be, there are probably other more important things you can work on.
Don’t optimize in the dark.
If you finally decided on optimizing your program, optimize for correctness first, not performance. All that being said, let’s study a couple of different scenarios to figure out how it’s decided whether to put a variable on the stack or on the heap.
— — —
*Disclaimer:
- GOVERSION: go1.20.1
- GOOS: linux
- GOARCH: amd64
- Inlining optimizations are disabled — — —*
Consider the example below where there is no pointers involved:
package main
func main() {
n := 4
n2 := square(n)
println(n2)
}
func square(x int) int {
return x * x
}
Why did I use println instead of fmt.Println? Just keep reading!
When starting the program, a stack “frame” is created for the func main with the values n = 4 and n2 = 0 . When the func square gets called, another frame is created for that function which houses x = 4 inside of it. After func square returns, the result n2 = 16 is going to be set in the first stack frame. Go does not clean up after itself, the second stack frame is still available in the stack and what go does is it keeps track of what is valid and what is invalid (the black line you can see in the image below). Finally when println is called a new frame is created for it with a = 16 and this new stack is placed in the valid section. The line moves up and down in order to define valid vs invalid area. This is why stacks are considered “self cleaning”, any variable on the stack is cleaned up as that space is reused (i.e. the addresses of x and a can be the same, and in fact they are).
Now, let’s add some pointers:
package main
func main() {
n := 4
inc(&n)
println(n)
}
func inc(x *int) {
*x++
}
Again, walking through this code in memory and skipping to the interesting part, when func inc is called, a new stack frame is created for it containing x = 0xc000044780 which actually points to the n = 4 in the first stack. When the pointer to n is dereferenced and incremented, the value of n in the first stack is updated and the second stack just get pushed into the invalid section. When reaching println, this function reclaims the space formerly owned by func inc and the process is continued as before. We are using pointers here but it was able to stay in the stack.
Sharing down typically stays on the stack.
Returning pointers?
package main
func main() {
n := answer()
println(*n/2)
}
func answer() *int {
x := 42
return &x
}
When we first call main it sets n = nil to begin with (in its original stack frame), as the func answer is called, the compiler already knows that it is not safe to put the variable x (created by func answer) on the stack frame, so instead x is declared somewhere on the heap. “Why?” you might ask, if the variable was going to be declared on the stack frame, then n would have to point to a value in the invalid section (that value being x = 42 and n would be something like 0xc000044770 and that would cause problems. We say that “x escaped to the heap”, but it doesn’t actually get moved at runtime, this happens at compile time. Compiler initially knows that this variable is going to be constructed on the heap.
Sharing up typically escapes to the heap.
The word “typically” both here and in the quote above, means that actually, Only the compiler knows.
According to the Golang FAQ:
When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors.
What we’re talking here is called “escape analysis”. It’s what compiler does, looking at our code to see if any of these variables need to be on the heap.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
So if only the compiler knows, let’s ask it!
go help build
usage: go build [-o output] [build flags] [packages]
...
-gcflags '[pattern=]arg list'
arguments to pass on each go tool compile invocation.
We use go build to build our programs, but the compiler actually is the go tool compile.
go tool compile -h
usage: compile [options] file.go...
...
-l disable inlining
...
-m print optimization decisions
# Example 1
go build -gcflags "-m -l" example1.go
# no output, since inlining is disabled
# Example 2
go build -gcflags "-m -l" example2.go
./example2.go:9:10: x does not escape
# Example 3
go build -gcflags "-m -l" example3.go
./example3.go:9:2: moved to heap: x
Here we can see that the compiler decides whether or not to put a variable on the heap at compile time.
But there’s a catch! You may have noticed that instead of fmt.Println which is arguably the common way to print out a variable, I considered using println. “Why is that?” you might ask, well, while playing out with fmt.Println I noticed something weird (to replicate these results, just replace println with fmt.Println in the examples above):
# Example 1
go build -gcflags "-m -l" example1.go
./example1.go:8:13: ... argument does not escape
./example1.go:8:14: n2 escapes to heap
# Example 2
go build -gcflags "-m -l" example2.go
./example2.go:11:10: x does not escape
./example2.go:8:13: ... argument does not escape
./example2.go:8:14: n escapes to heap
# Example 3
go build -gcflags "-m -l" example3.go
./example3.go:11:2: moved to heap: x
./example3.go:7:13: ... argument does not escape
./example3.go:7:17: *n / 2 escapes to heap
It seems to be an open issue on Golang GitHub:
So let’s wrap up. When are values constructed on the heap?
When a value could possibly be referenced after the function that constructed the value returns.
When the compiler determines a value is too large to fit on the stack.
When the compiler doesn’t know the size of a value at compile time (i.e. in the case of a slice).
Some commonly allocated values (not exhaustive):
Values shared with pointer.
Variables stored in interface variables.
func literal variables (or anonymous functions).
Backing data for map, channel, slices and string (strings are effectively a special slice of bytes).
Which one to choose?
After all the discussions above, it might still be unclear, let’s say, whether to pass a copy of a struct or the pointer to it, or to use a pointer receiver or not. Hence, let’s review some opinions from various sources.
An overall conclusion is perfectly depicted here:
Methods using receiver pointers are common; the rule of thumb for receivers is, “If in doubt, use a pointer.”
Slices, maps, channels, strings, function values, and interface values are implemented with pointers internally, and a pointer to them is often redundant.
Elsewhere, use pointers for **big structs **or structs you’ll have to change, and otherwise pass values, because getting things changed by surprise via a pointer is confusing
Another informative discussion can be found here:
Is the struct reasonably large? Prematurely optimizing is rarely good, but if you have a struct with more than a handful of fields and/or fields containing large strings or byte arrays (e.g. a markdown body) the benefits to returning a pointer instead of a copy becomes more apparent.
What’s the risk of the returning function mutating the struct (or object) after it returns it? (i.e. some long-running task or job)
I nearly always return a pointer from a constructor as the constructor should be run once.
Also in the same discussion William Kennedy pointed out:
I have a bit of a different philosophy. Mine is based on making a decision about the type you are defining. Ask yourself a single question. If someone needs to add or remove something from a value of this type, should I create a new value or mutate the existing value. This idea comes from studying the standard library.
There are certain situations in which we (mistakenly) assume that a poor performance is mainly due to not utilizing pointers well, but with a little bit of more evaluation, we figure out a new way of doing things (e.g. a better design) in order to improve the performance of the software in a readable and idiomatic way, without adding to the complexity and confusion of the code by introducing unnecessary pointers. Also as the Zen of Go suggests:
If you think it’s slow, first prove it with a benchmark. In practice, I have never had to care about the level of performance where the difference between a pointer and a non-pointer mattered. I believe that the instances where that might matter are very rare.
Summary
In this article (actually my first one as well!) I tried to explore the mysterious area of pointers and to answer the question of whether or not to use a pointer.
My own deduction with regards to the references above is to try not using a pointer. If faced with poor performance, first just prove it with a benchmark and don’t make unfounded assumptions about performance. If you’ve made sure that you actually *do *have performance issues, reevaluate the code in the hope of coming up with a better design. If failed to do so, or for whatever reason that design change didn’t seem reasonable, then I guess you have no choice but to use a pointer.
Regardless, there are certain situation in which it seems reasonable to use a pointer. Some of the more common ones being:
You’re dealing with a changing struct.
You’re dealing with a mutex, which is somehow the same idea as a changing struct (i.e. it’s constantly getting locked and unlocked).
Your struct is particularly big (maybe in case of a config struct or something, with an imaginary threshold of like a few hundred bytes).
We’ve seen that despite the “memory efficient” nature of pointers (since they prevent a whole struct getting copied, instead only the pointers themselves are getting copied), if not used properly, they tend to generate a lot of overhead and cause performance degradation as well as introducing unnecessary complexity and confusion.
Finally, I wanted to thank you for reading this blog, and hopefully that wasn’t a total waste of your time! I’d love to learn more technical stuff from you guys and would love to hear your suggestions and opinions about my writings. You can reach me via LinkedIn and Gmail.
Top comments (0)