Understanding Values and Pointers in Go.
Today, I'm going to talk about values and pointers in Go, which many find confusing.
Unless we're dealing with some super-specific performance situation, we're going to discuss using a pointer
only when we want our function to mess around and change the value it receives.
For all other cases, let's just stick with the value.
Let me break down why I'm saying this. Firstly, let's talk about semantics.
When we use a pointer, we're basically putting a label on whether our object can be tampered with by the function getting it. Now, things get a bit tricky with reference types like slices and maps because, even though they're passed by value, they have these internal pointers to shared storage underneath - essentially, these types are implemented as Go structures themselvses. It's like a little quirk we need to keep in mind while getting cosy with Go.
Since Go is a call by value language, the values passed to functions are copies. For nonpointer types like primitives, structs, and arrays, this means that the called function cannot modify the original. Since the called function has a copy of the original data, the immutability of the original data is guaranteed. However, if a pointer is passed to a function, the function gets a copy of the pointer. This still points to the original data, which means that the original data can be modified by the called function.
Moving on to performance – not all memory is created equal. Objects passed by value usually chill out in the stack memory, which is pretty cool and doesn't require any manual freeing (in simple terms, the memory just sticks around on the stack until the next function comes along and takes its place). On the flip side, objects linked by pointers might end up on the heap. Go's escape analysis sometimes can't decide if they'll outlive the function they're in, so it plays it safe and tosses them on the heap. Now, the heap is a bit more high-maintenance, requiring extra effort to allocate and bringing along a cleanup crew known as the Garbage Collector (GC). It roams the heap, looking for data that no code cares about, and frees up the memory. When we're comparing the performance of these two approaches, keep in mind that benchmarking might not show us the real cost of heap usage because the GC might not even collect the memory during a benchmark. - therefore, we're not really thinking about the cost of the heap operations.
Now, let's touch on concurrency safety. Objects passed as copies (values) are like solo adventurers – safe for concurrent use because they keep their data to themselves. Of course, there's a little twist when it comes to reference types like maps and slices, as they have that sneaky internal pointer to shared data. So, if we embrace this style, our code becomes more naturally open to the benefits that concurrency brings.
Of course, there are exceptions to every rule. Go might throw very large data onto the heap regardless of its escape plans. Also, in some cases, passing pointers to massive values – like hefty binary or string blobs – could actually boost performance. So, it's all about finding that sweet spot and making exceptions when needed.