Logo

dev-resources.site

for different kinds of informations.

An ode to Stacks and Pointers in Go!

Published at
5/11/2024
Categories
go
pointers
stack
Author
__init__abs
Categories
3 categories in total
go
open
pointers
open
stack
open
Author
11 person written this
__init__abs
open
An ode to Stacks and Pointers in Go!

Introduction

Let's be honest: pointers can be a tough nut to crack. Misusing them can lead to frustrating bugs and slow performance, especially in concurrent or multi-threaded programs. That's why many languages shield developers from dealing with pointers altogether. However, if you're coding in Go, you can't escape them. Mastering pointer is essential for crafting elegant, efficient code in Go.

Function Frames

In Go programming, functions execute within a defined function frame, each maintaining their distinct memory space. These frames allow the functions to operate autonomously within their designated context, facilitating smooth flow control. While a function can readily access memory within its frame, venturing beyond its frame necessitates indirect access. To tap into the memory within a different function frame, the memory in question must be shared with the function via frame pointer. Grasping the intricacies and constraints imposed by these function frames is paramount.

When a function is invoked within another function, there is a transaction that occurs between the calling and the called function frame. If the function call requires data, then the data must be passed to the other frame in "pass by value" fashion.

Pass by value (also known as pass-by-copy) is the argument passing technique utilized in the Go programming language. This technique permits various forms of arguments in the call, such as constants, variables, and complex expressions, while also ensuring the immutability of arguments. It achieves this by guaranteeing that the function receives a copy of the data. Consequently, the function can modify the data without impacting the original data.


1. package main
2. 
3. import "fmt"
4. 
5. func increment(val int) {
6.  // Increment the value of val
7.  val++
8. 
9.  // Printing the value of the val variable
10.     fmt.Println("Val value:", val)
11. 
12.     // Printing the address of the val variable
13.     fmt.Println("Val Address:", &val)
14. 
15. }
16. 
17. func main() {
18.     // Declaring variable of type int
19.     var counter int
20. 
21.     // Setting the value of the counter variable to 1
22.     counter = 1
23. 
24.     // Printing the value of the counter variable
25.     fmt.Println("Counter value:", counter)
26. 
27.     // Printing the address of the counter variable
28.     fmt.Println("Counter Address:", &counter)
29. 
30.     // Pass counter as an argument to the increment function
31.     increment(counter)
32. 
33.     // Printing the value of the counter variable
34.     fmt.Println("Counter value after increment:", counter)
35. 
36.     // Printing the address of the counter variable
37.     fmt.Println("Counter Address after increment:", &counter)
38. 
39. }
Enter fullscreen mode Exit fullscreen mode

When executing a Go program, the runtime initiates the main goroutine to commence executing all code, including that within the main function. A goroutine represents a path of execution assigned to an operating system thread for execution on one of the cores. Each goroutine is assigned an initial ~2KB block of contiguous memory, forming its stack space.

The stack serves as the physical memory location for each function frame. The image below illustrates the physical memory allocated to the main function frame on the stack.


Figure 1 - Main Function Frame

In Figure 1, a segment of the stack is delineated for the main function, forming what is known as a "Function Stack Frame". This frame serves to demarcate the main function's boundary within the stack and is created during the execution of the function call. Additionally, within the main frame, memory for the counter variable is located at address 0xc000012028.

Function Calls

The main function calls the increment function as shown on line 31. A new function call means that the goroutine needs to create a new stack frame for increment function(called function). To successfully execute this function call, data needs to be passed across the stack frame and placed into the newly created stack frame as specified in the declaration of the increment function on line 5. In this specific case, an integer value(counter) is expected to be copied and passed during the call.

As the increment function can only read and write to memory locations within its own frame, it needed a variable val of type int to store and access its own copy of the counter value being passed. The passed value of counter is copied and passed into the newly created val variable inside the increment function frame.


Figure 2 - Main & Increment Frames

You'll notice that the stack now comprises two frames: one for the main function and, beneath it, one for the increment function. Within the increment frame, the variable val holds the copied value of 1 passed during the function call. Positioned at address 0xc0000a2018, the val variable resides lower in memory, reflecting the sequential arrangement of frames along the stack. This ordering is merely an implementation detail devoid of significance. Crucially, the goroutine extracted the counter value from the main frame and duplicated it within the increment frame using the val variable.

The control executes all the lines under the increment function and the output on the terminal should look something like this:

Val value: 2
Val Address: 0xc0000a2018
Enter fullscreen mode Exit fullscreen mode

Function Returns

After executing all the lines under the increment function, the control returns to the main function with a small change to the stack frames.

The stack frame associated with the increment function is now part of the garbage memory because the control has shifted to main function thereby making the main function frame the active frame. The memory that was framed for the increment function is left untouched.

It's pointless to tidy up the memory of the returning function's frame because there's no way to know if that memory will be needed again. Hence, the memory remains unchanged. The stack memory for each frame is actually wiped clean during every function call. This cleaning process happens when values are initialized within the frame. Since all values are set to their default "zero value"(0 is the default value for the int type) during initialization, the stacks naturally tidy themselves up with each function call.


Figure 3 - Frames after increment func returns

The final output of the above code block on the terminal should look something like this:

Counter value: 1
Counter Address: 0xc000012028
Val value: 2
Val Address: 0xc0000a2018
Counter value after increment: 1
Counter Address after increment: 0xc0000a2010
Enter fullscreen mode Exit fullscreen mode

Sharing Values

In case if it was important for the increment function to operate directly on the counter variable that exists inside the main function's stack frame, pointers prove invaluable. They facilitate value sharing, enabling functions to operate on variables located outside their own stack frame.

Indirect Memory Access

The code block below performs a function call passing an address "by value". The value of the counter variable from the main stack frame is shared with the increment function.


1. package main
2. 
3. import (
4.  "fmt"
5. )
6. 
7. func increment(val *int) {
8.  // Increment the value of val
9.  *val++
10. 
11.     // Printing the value of the val variable
12.     fmt.Println("Val value:", val)
13. 
14.     // Printing the address of the val variable
15.     fmt.Println("Val Address:", &val)
16. 
17.     // Printing the value the val pointer points to
18.     fmt.Println("Val Points To:", *val)
19. 
20. }
21. 
22. func main() {
23.     // Declaring variable of type int
24.     var counter int
25. 
26.     // Setting the value of the counter variable to 1
27.     counter = 1
28. 
29.     // Printing the value of the counter variable
30.     fmt.Println("Counter value:", counter)
31. 
32.     // Printing the address of the counter variable
33.     fmt.Println("Counter Address:", &counter)
34. 
35.     // Pass counter as an argument to the increment function
36.     increment(&counter)
37. 
38.     // Printing the value of the counter variable
39.     fmt.Println("Counter value after increment:", counter)
40. 
41.     // Printing the address of the counter variable
42.     fmt.Println("Counter Address after increment:", &counter)
43. 
44. }

Enter fullscreen mode Exit fullscreen mode

The three interesting changes that were made to facilitate indirect memory access are as follows:

  1. On line 36, the code is not copying and passing the "value of" counter but instead the "address of" counter. "&" operator extracts the address of the counter variable in the main function frame. As Go is a pass by value language, so we are still passing a value but just in form of an address.

  2. On line 7, *int highlights that the increment function expects a address of the variable(pointer) rather than the variable itself. Since the variable counter is of type int, it's pointer type is *int.


Figure 4 - Frame Stack after func call to increment

  1. On line 9, the * character in *val++ is acting as an operator and extracts the value that pointer is pointing to. The pointer variable allows indirect memory access outside of the function's frame. The process of extracting the the value that pointer is pointing to is called dereferencing.


Figure 5 - Frame Stack after executing line 9

The final output of the above code block on the terminal should look something like this:

Counter value: 1
Counter Address: 0xc000012028
Val value: 0xc000012028
Val Address: 0xc00004a028
Val Points To: 2
Counter value after increment: 2
Counter Address after increment: 0xc000012028
Enter fullscreen mode Exit fullscreen mode
stack Article's
30 articles in total
Favicon
Stack Developer Web
Favicon
Whats your TECH stack ?? How did you get into that??
Favicon
Building a Stack Implementation in Java: Mastering Data Structure Fundamentals.
Favicon
Pattern 7: Stack
Favicon
Stacks: 50 Leetcode Questions
Favicon
Why Is Stack Memory Faster Than Heap Memory? Hereโ€™s What You Need to Know!
Favicon
Stack: Concepts and Applications โ€” Java
Favicon
Top 7 Reasons to Hire a MERN Stack Development Company for Scalable Web Solutions
Favicon
Maximum swap
Favicon
Comprehensive ๐—š๐˜‚๐—ถ๐—ฑ๐—ฒ ๐˜๐—ผ ๐—ฆ๐˜๐—ฎ๐—ฐ๐—ธ ๐——๐—ฎ๐˜๐—ฎ ๐—ฆ๐˜๐—ฟ๐˜‚๐—ฐ๐˜๐˜‚๐—ฟ๐—ฒ: ๐—œ๐—บ๐—ฝ๐—น๐—ฒ๐—บ๐—ฒ๐—ป๐˜๐—ฎ๐˜๐—ถ๐—ผ๐—ป, ๐—ข๐—ฝ๐—ฒ๐—ฟ๐—ฎ๐˜๐—ถ๐—ผ๐—ป๐˜€, ๐—ฎ๐—ป๐—ฑ ๐—ฃ๐—ฟ๐—ผ๐—ฏ๐—น๐—ฒ๐—บ-๐—ฆ๐—ผ๐—น๐˜ƒ๐—ถ๐—ป๐—ด
Favicon
Understanding Stack as an Abstract Data Type
Favicon
Heap vs Stack: como o Java gerencia o que deve ser lembrado ou esquecido
Favicon
Factors to consider in choosing a tech stack for a project
Favicon
stack in PyTorch
Favicon
Becoming a Full Stack Developer: A Step-by-Step Guide
Favicon
Full Stack Development: A Comprehensive Guide
Favicon
The Complete Guide to Full Stack Development: Essential Skills and Strategies
Favicon
Stack & Heap
Favicon
Stacks in Action: A Dive into the Concept and Implementation
Favicon
An ode to Stacks and Pointers in Go!
Favicon
Queue and Stack Essentials: A Python Programmer's Guide
Favicon
Navigating the Seas of Web Development
Favicon
Stack are not Queue
Favicon
Solving DSA Problems. HackerRank: Balanced Brackets
Favicon
Understanding Stack Memory and Heap Space inย Java.
Favicon
Join Our Stack Programming Community!
Favicon
Understanding the Stack Data Structure: A Java Implementation
Favicon
A Brief Look At Spotify's Tech Stack
Favicon
Implementation of Stack Using Array
Favicon
Detect what calls your Node.js code

Featured ones: