DEV Community

Amarachi Iheanacho
Amarachi Iheanacho

Posted on

Understanding memory allocation in Go using Go pointers

Go pointers are the gift that keeps on giving. They allow you to manipulate memory efficiently, create custom data types, and work seamlessly with complex data structures. This efficiency translates to cleaner, more performant, and flexible code. However, there's a catch: pointers can be quite tricky to master, and using them incorrectly can lead to dangling pointer errors and even memory leaks.

This article dives into the world of Go pointers, covering:

  • Pointer definition, basic syntax, and why their importance
  • How it works in collaboration with memory allocation in Golang.
  • Practical applications and best practices for using pointers effectively

Variables and the need for pointers

To better understand what Go pointers are and why they are so interesting, lets take a look at their relationships with variables - the cells of a program.

Chances are, you've already encountered variables in your software development journey. They're a fundamental concept.

You can think of variables as a simple box with a label. The label shows the variable's name and the data type it holds, like numbers (integers) or text (strings). These boxes are stored in a vast warehouse alongside countless others.

To find a specific box, especially buried in a pile, you need an address system. You might ask, "What section is this box in? What row or column?" This concept of location is similar to how pointers work in programming.

Pointers are fascinating tools used to store the memory address of other variables. You can think of them as little arrows stuck on boxes. These arrows don't hold the data themselves, but they point to the exact location in memory where another variable's data resides. They act as guides, leading you to the valuable information stored elsewhere.

To create a pointer, there are a few parts to care about:

  • Declaration: You declare a pointer variable using an asterisk (*) before its data type. For example, var ptr *int declares a pointer to an integer. This means that the ptr variable will hold the memory address of an integer.
  • Initialization: To make a pointer hold the memory address of a variable, you use the address-of operator (&). For example, ptr = &num assigns the memory address of variable num to the pointer ptr.
  • Dereferencing: To access the value stored at the memory location pointed to by a pointer, you use the dereference operator (*). For example, *ptr retrieves the value stored at the address ptr points to.

Bringing all the parts together, here is how a basic pointer syntax looks:


package main
import "fmt"
func main() {
    //create the variable
    x := 27
    fmt.Println(x, "is the original") //print out the variable
    //declare the pointer variable
    var y *int
    // initialize the pointer
    y = &x
    // prints out the address of x
    fmt.Println(y, "is the address of x")
    //dereference the pointer variable
    z := *y
    fmt.Println(z, "the value that y is pointing to")

}

Enter fullscreen mode Exit fullscreen mode

In the code block above, you:

  • Create a variable x that holds the value 27
  • Declare a pointer variable y that will hold the memory address of an integer; in this case, variable x
  • Use this code line y = &x to store the memory address of x in the y variable
  • Retrieve the value the memory address in y is pointing to

Output:

Image description

Why do you need Pointers?

Now that you understand what pointers are, what they look like, and how they work, let’s get into what exactly makes them interesting.

One of the largest reasons you need Go pointers is efficient memory management. Go uses pass-by-value for function arguments, meaning when you pass in a value as a parameter to a function, the function only receives a copy of the passed in data, and not the original.

This pass-by-value feature offers immutability but can quickly become inefficient, especially for large data structures. Ideally, you want to store your variables in one central location and edit them directly when needed rather than creating unnecessary copies throughout your code. Pointers let you do that, and more efficiently, too.

Instead of passing copies of data during function calls, pointers allow you to pass the memory address of the data itself. This approach is also significantly more efficient than passing the entire dataset, saving memory, and improving performance.

To truly appreciate why pointers are essential for manipulating variables across function calls, let's take a moment to understand how Go manages memory allocation.

Connecting pointers to memory allocation: Stacks and Heaps

Go has two main memory regions: the stack and the heap. The stack is a faster but fixed-size memory area used for local variables within functions. The heap is a more dynamic memory space that grows and shrinks as needed.

The next couple of sections discuss stacks and heaps in more detail. They use pointers to explain key concepts such as escape analysis, dangling pointers, and garbage collection.

Stacks

When you initiate calls in Go, a lightweight process called a goroutine is created. Each goroutine has its own memory stack for storing local variables and function call information.

When a goroutine makes a function call, a portion of its stack is allocated as a frame. This frame holds information about the function's arguments and local variables. After the function finishes running, its corresponding frame disappears from the stack.

Frames cannot directly access data in other frames, which agrees with Golang’s pass-by-value feature.

To better explain this phenomenon, lets intialize a variable called name in the main() function, and then try to edit the value of the name variable using another function called updateName().


package main
import "fmt"
func updateName(name string) {  // Renamed function for clarity
  name = "Joanna" // This creates a new string variable "Joanna"
  // but doesn't modify the original name variable passed to the function
}

func main() {
  name := "John"
  updateName(name)  // Pass a copy of the name variable
  fmt.Println(name)  // Prints "John", the original value
  // because updateName only operates on a copy

}

Enter fullscreen mode Exit fullscreen mode

Output:
Image description

In the code block provided, the main() function initializes a variable name with the value "John". This variable is passed to the updateName() function in an attempt to modify its value. However, after printing the value of name in the main() function, it remains unchanged even after updateName is called. This is because the function only modifies a copy of the name variable, leaving its original value intact.

Pointers offer a clever solution. By creating a pointer and passing it to a function, you essentially provide the function with the memory address of the original variable. Even though the function receives a copy of this address (not the data itself), it can use this address to locate and modify the original variable's value.

So even after this function finishes running and its corresponding frame disappears, the changes it made during its life still stand.

For example:


package main
import "fmt"
func updateName(name *string) { //passes the pointer as a parameter and signifies that it’s a pointer type of string (*string)
  *name = "Joanna" // Dereferences the name pointer and modifies it’s value

}

func main() {
  name := "John"
  addressName := &name
fmt.Println(addressName) // Prints "Joanna", the updated value
  updateName(addressName) // Passing the address of the name variable
  fmt.Println(name) // Prints "Joanna", the updated value

}

Enter fullscreen mode Exit fullscreen mode

Output:
Image description

Now, stacks are great for temporary data within functions, but what about data that needs to persist beyond a function's lifetime? That's where heaps come in.

Heaps

Imagine a function called initPerson that creates a person object. You might want to pass this object to the main function for further processing. However, there's a catch: if you simply pass a pointer to the main function referencing the object created in initPerson, a problem arises.

Once initPerson finishes its execution, and its stack disappears, the variable holding the object itself is gone. This leaves the pointer in the main function pointing to nowhere, creating a situation known as a dangling pointer.

For example:

package main

import "fmt"

type person struct {
   name string
}

func initPerson() *person {
   p := person{name: "John"}
   return &p // Returning a pointer to the Person object
}

func main() {
   p := initPerson() // Getting a pointer to the Person object

   // At this point, p points to a Person object created in initPerson()

   // Now, let's imagine further processing of the Person object in main() function
   fmt.Println("Name:", p.name)

   // However, there's a problem here. Once initPerson() finishes and its stack disappears,
   // the variable holding the Person object (p) is gone. The pointer in the main function
   // would then be pointing to nowhere, causing a dangling pointer issue.
}
Enter fullscreen mode Exit fullscreen mode

By storing the p object on the heap, it can outlive the initPerson function's lifetime. Go achieves this with the help of escape analysis, a powerful optimization technique that efficiently handles memory allocation and management.

Escape Analysis: Automatic Optimization

In Go, escape analysis is a crucial compiler optimization technique that determines where to allocate variables: on the stack or the heap.

This technique examines the way variables are used within a function's scope. During compilation, it analyzes if a variable's value can potentially be accessed outside the function (e.g., returned from the function, passed by reference to another function, stored in a global variable).

If the compiler CANNOT prove that the variable is NOT referenced after the function returns, the compiler will typically allocate the variable on the heap to avoid dangling pointer errors. This phenomenon is discussed in the Go FAQ section “How do I know whether a variable is allocated on the heap or the stack?”

So, with the initPerson() function example from earlier, you still get the correct value even though you try to get its Name field after the function returns.

Image description

To prove that the Name field does in fact escape to the heap, build out your main.go file with this command:


go build -gcflags="-m -l" main.go

Enter fullscreen mode Exit fullscreen mode

Output:

Image description

As handy as heaps are, it is important to know that only the compiler knows which variables will be on the stack or the heap. Generally, sharing down typically stays on the stack, and sharing up escapes to the heap.

Here are some examples of scenarios where values are commonly constructed on the heap:

  • When a value could possibly be referenced after its parent function 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 the compile time

Heaps and Garbage Collection

While heaps offer flexibility, overuse can burden the garbage collector. Unlike stacks, which are automatically cleaned, heaps require periodic scanning to identify and reclaim unused memory. An excessively large heap can lead to performance issues due to increased garbage collection time.

That’s a Wrap!

This article took you on a journey in the world of Go pointers, your key to unlocking new levels of efficiency and flexibility in your code. It explored the fundamentals, including what pointers are, how to use them (syntax), and why they're so valuable. It also discussed how pointers work with memory allocation (stacks and heaps) and the potential pitfalls to avoid.

While this covered a lot, there's always more to learn!

The most important thing is practice. Write functions that leverage pointers to manipulate data efficiently. Good luck.

Top comments (2)

Collapse
 
fred_functional profile image
Fred Functional

Could you provide more examples on how to debug common pointer-related issues in Go? This topic would complement the well-explained basics and practical advice in the post. Thanks for the detailed overview!

Collapse
 
amaraiheanacho profile image
Amarachi Iheanacho

Hey Fred, Sure I can. I will create an article on this.