Understanding Stack and Heap Memory in Go

📚 5 min read Tweet this post

As a programmer, you may have heard the terms “stack” and “heap” used to describe different types of memory in your code. In Go, these concepts are particularly important because the language’s automatic memory management system relies heavily on them. In this article, we’ll take a closer look at what the stack and heap are, how they work, and how they are used in Go programming.

The stack is a region of memory that is allocated for each thread of execution in your program. It is used to store function call frames, which contain information about the local variables, function parameters, and return addresses of each function call. When a function is called, a new frame is pushed onto the stack, and when the function returns, the frame is popped off the stack.

One of the key benefits of using a stack is that it allows for very fast access to memory. Because the most recently pushed item is always at the top of the stack, accessing it requires only a simple pointer adjustment. This makes the stack ideal for managing the temporary data that is associated with function calls.

The heap is another region of memory that is used to store data in your program. Unlike the stack, however, the heap is not managed automatically by the compiler or runtime. Instead, you are responsible for allocating and freeing memory on the heap as needed.

Because the heap can grow dynamically at runtime, it is often used to store data that is too large to fit on the stack, or that needs to persist beyond the lifetime of a function call. However, because the heap is not managed automatically, it is also more prone to memory leaks and other types of errors if not used carefully.

In Go, the compiler and runtime work together to manage memory automatically, using a technique called garbage collection. This means that you don’t need to worry about manually allocating or freeing memory on the heap. Instead, the runtime will automatically identify and clean up any unused memory for you.

However, while the Go runtime handles most of the memory management automatically, you still need to be aware of how the stack and heap are used in your program. For example, if you allocate large data structures on the stack, you may run out of stack space quickly and crash your program. On the other hand, if you allocate too much memory on the heap, you may cause unnecessary memory pressure and slowdowns in your program.

Here’s an example of how the stack and heap are used in Go:

func main() {
    // Allocate a large array on the stack
    var arr [10000000]int

    // Allocate a smaller slice on the heap
    slc := make([]int, 1000)

    // Call a function that allocates memory on the heap
    foo()
}

func foo() {
    // Allocate a large slice on the heap
    slc := make([]int, 1000000)

    // Do some work with the slice
    for i := 0; i < len(slc); i++ {
        slc[i] = i
    }
}

In this example, we allocate a large array on the stack, a smaller slice on the heap, and call a function that allocates a large slice on the heap. Because the array is allocated on the stack, it is limited in size and can potentially cause a stack overflow if made too large. However, because the slices are allocated on the heap, they can grow dynamically as needed.

package main

import "fmt"

func function1() {
    var1 := 5
    var2 := 7
    fmt.Println("function1 variables:", var1, var2)
}

func function2() {
    var1 := 2
    var2 := 3
    fmt.Println("function2 variables:", var1, var2)
}

func main() {
    // Call function1 and function2, which each create their own call frames on the stack.
    function1()
    function2()

    // Allocate three data structures on the heap using the new() keyword.
    data1 := new(int)
    *data1 = 10
    data2 := new(string)
    *data2 = "hello"
    data3 := new(float64)
    *data3 = 3.14

    // Print out the memory addresses of each data structure.
    fmt.Println("data1:", data1)
    fmt.Println("data2:", data2)
    fmt.Println("data3:", data3)
}

In this code, the function1() and function2() functions each create their own call frames on the stack with their own local variables. When main() calls each function, it creates a new call frame on the stack, pushes it onto the top of the stack, and then pops it off the stack once the function returns.

The new() keyword is used to allocate memory on the heap for three different data structures: an integer, a string, and a float64. Each data structure is assigned a value and then printed out along with its memory address.

The memory allocated for the call frames is automatically reclaimed by the Go garbage collector once the function returns, while the memory allocated on the heap must be manually deallocated using the delete keyword or by allowing the Go garbage collector to reclaim it.

See the simplified illustration below to better undestanding how memory allocation mapped in stack and heap from the example above.

+---------------+        +---------------+
|   Stack       |        |   Heap        |
+---------------+        +---------------+
|  Function 2   |        |  (ptr1) ------|----> (data1)
|  Call Frame   |        |  (ptr2) ------|----> (data2)
|  (var1 = 2)   |        |  (ptr2) ------|----> (data2)
|  (var2 = 3)   |        +---------------+
|  (ret addr)   |
+---------------+
|  Function 1   |
|  Call Frame   |
|  (var1 = 5)   |
|  (var2 = 7)   |
|  (ret addr)   |
+---------------+

In Go, the stack and heap are both used for managing memory, but they serve different purposes. The stack is used for storing local variables, function call frames, and other small data structures that are created and destroyed quickly. Each function call creates its own call frame on the stack, and the call frame is popped off the stack when the function returns.

The heap, on the other hand, is used for storing larger data structures that may persist beyond the lifetime of a function call. Memory allocated on the heap must be manually managed, either by explicitly freeing the memory using the delete keyword or by allowing the Go garbage collector to reclaim it.

Understanding the difference between the stack and heap is important for managing memory efficiently and avoiding memory leaks and other issues. By using the stack for short-lived data structures and the heap for longer-lived data structures, developers can ensure that their code uses memory in a way that is both safe and efficient.

programming go practical heap stack