Demystify Go Pointers

Introduction to Pointers

A fundamental all performance engineers know and employ is good memory management, one such area is understanding how Pointers work in your chosen language and how variables interact with memory.
Go (Golang) which is a statically typed language developed by Google, offers a unique approach to pointers. Often newcomers to the language, struggle with understanding pointers especially if they are coming from dynamic languages like Python or JavaScript, even though they are using references in certain instances eg: objects and classes, these languages often abstract pointers away.

Let us demystify this often confusing topic by running through a set of examples, and hopefully, when we are done you take some pointers home with you - no pun intended!

Mastering pointers is a significant step towards becoming a proficient engineer in any programming language.

 
What are Pointers?

A pointer is a variable that stores the memory address of another variable.
This means that instead of holding a data value itself, a pointer holds the location of where the value is stored in memory. By knowing the address, the pointer can access and modify the value at that location, even if it's outside its scope.
Pointers are powerful tools in programming, enabling indirect interaction with and alteration of the values of targeted variables. This capability is particularly useful for manipulating large data structures without the need to copy or duplicate them, significantly enhancing the efficiency and performance of your code.

Pointers in Go

Go's overall design philosophy, prioritizes simplicity, safety, and efficiency - therefore Go has no support for pointer arithmetic. This design choice reduces the complexity and potential for errors, such as buffer overflows, which are common pitfalls in languages that allow direct memory manipulation through pointers.
In Go, pointers are primarily used to access and modify the data stored in the memory address of a variable. This is similar to other languages but is done in a way that encourages more straightforward and safer code. By eliminating pointer arithmetic, Go ensures that pointers are less prone to misuse, making programs more robust and maintainable. Additionally, Go's garbage collector further reduces the risk associated with pointers by automatically managing memory allocation and deallocation, preventing common issues like memory leaks and dangling pointers.

Improper use of pointers in Go can lead to insidious bugs, often culminating in the much-dreaded panic: Go's way of screaming 'something went terribly wrong!

Declaring and Using Pointers in Go

Let us review a basic example of pointers and break it down, take note of the following program:

package main

import "fmt"

func main() {
	var a int = 10
	var p *int = &a // p stores the address of a

	fmt.Println("Value of a:", a)    // Output: Value of a: 10
	fmt.Println("Address of a:", &a) // Output: Address of a: [memory address]
	fmt.Println("Value of p:", p)    // Output: Value of p: [same memory address as a]
	fmt.Println("Value at p:", *p)   // Output: Value at p: 10

	*p = 20                           // changing the value at the address p points to
	fmt.Println("New value of a:", a) // Output: New value of a: 20
}

Output

Value of a: 10
Address of a: 0xc000112000
Value of p: 0xc000112000
Value at p: 10
New value of a: 20
Why did the value of the variable ”a" change?

Let's break this down, so we get the full picture:

  1. An integer value of 10 is assigned to the variable a

  2. A new pointer variable p is created and is assigned the address of variable a - not the value

    1. The “&” operator is used to fetch the memory address of a variable.

This means that p does not hold the value 10 itself; rather, it holds the memory location or address of where the value 10 is stored (the address of a).
When you dereference p using the *p operation, you access the value 10, which is the value stored at the address that p points to.

Pointers Unveiled

image-20240221-152415

A Picture is Worth a Thousand Bytes, breaking this diagram down, we can see that:

  • variable "a" is a normal variable holding the value 10

  • variable "p" is a pointer variable holding the memory address of variable “a"

  • Both variables a and *p will yield the value 10, but p itself contains an address, not the integer value.

    • The * operator is used to dereference the pointer when printing the value.

image-20240221-152900

A Gopher climbing the proverbial pass by value or reference!
image source: github.com/egonelbre/gophers

 

Pass by value vs Pass by reference

Let us look at another example to solidify our understanding of pointers, by climbing into the pass-by-value or reference example below.

Notice in the following program:

  • The memory address of the variable in passByValue is always different

    • If we added code to change the value, it would have no effect outside of this function.

  • The memory address of the variable inpassByReference is the same as the main variable

    • Changing the value affects the main program, as it is the same “memory” address.

package main

import "fmt"

// passByValue prints the value and its memory address.
func passByValue(val int) {
	fmt.Printf("passByValue - Value: %d, Address: %p\n", val, &val)
}

// passByReference prints the value that the pointer points to and its memory address.
// It also modifies the original value through the pointer.
func passByReference(ref *int) {
	fmt.Printf("passByReference - Value before modification: %d, Address: %p\n", *ref, ref)
	*ref = 20
	fmt.Printf("passByReference - Value after modification: %d, Address: %p\n", *ref, ref)
}

func main() {
	a := 10

	fmt.Printf("main - Value of a: %d, Address of a: %p\n", a, &a)
	passByValue(a)
	fmt.Printf("main - Value of a: %d, Address of a: %p\n\n", a, &a)

	fmt.Printf("main - Value of a: %d, Address of a: %p\n", a, &a)
	passByReference(&a)
	fmt.Printf("main - Value of a: %d, Address of a: %p\n", a, &a)
}

Output

main - Value of a: 10, Address of a: 0xc000112000
passByValue - Value: 10, Address: 0xc000112008  <------- different memory address
main - Value of a: 10, Address of a: 0xc000112000

main - Value of a: 10, Address of a: 0xc000112000
passByReference - Value before modification: 10, Address: 0xc000112000
passByReference - Value after modification: 20, Address: 0xc000112000 <--- same memory address
main - Value of a: 20, Address of a: 0xc000112000
Losing your mind yet?
  • Note the definition of the function passByValue - it is requesting a value of int and so therefore whenever we call this method we create a “copy” or a “copy of the value” and run the function.
    • Pseudo-code

      a := 10
      passByValue(a)
      extract the "value" from variable a and assign it to varaible val - value copied
      execute passByValue function
  • Note the definition of the function passByReference - we are requesting a pointer of int Therefore whenever we call this method we DO NOT want a copy, but rather a variable that points to a value or aptly called a reference.

    • Pseudo-code

      a := 10
      passByReference(&a) // parameter is a pointer to a value
      assign the pointer to variable ref - no copies made
      execute passByReference function, any modifications will affect the value of a

 

Comparing Go Pointers with Other Languages

Go vs. C/C++

  • Pointer Arithmetic: C/C++ allows pointer arithmetic; Go does not. In Go, a pointer is more about referencing and dereferencing, making it safer.

  • Safety: Go provides a safer environment by avoiding dangling pointers (pointers pointing to freed memory) and null pointer dereferencing.

  • Garbage Collection: Go has built-in garbage collection, which manages memory allocation and deallocation, unlike C/C++, where manual memory management is required.

Go vs. Java

  • Direct Memory Access: Java does not in the traditional sense have pointers or provide direct memory access - however this is possible through Java JNI or Project Panama. Compared to Go, it does allow direct memory access, via its pointer mechanism.

  • Control: Go gives more control to the programmer over how memory is accessed and manipulated, while Java abstracts it away for safety and simplicity.

Conclusion

Pointers in Go strike a balance between power and safety. They provide more control over memory than in higher-level languages like Java, while avoiding many complexities and safety issues present in languages like C and C++. Understanding pointers is crucial for mastering Go, as they are a key part of the language's efficiency and power.

For programmers coming from different backgrounds, grasping Go's pointer system requires a shift in thinking, but it's a worthwhile investment for the efficiency and safety it brings to your code.

 

References:
Back to Blog