Pointers Uncovered: A beginners guide  in Go

Photo by RetroSupply on Unsplash

Pointers Uncovered: A beginners guide in Go

Pointers in Go are a fundamental concept, similar to pointers in other programming languages like C or C++. They hold the memory address of a value rather than the value itself. Understanding pointers is crucial for managing memory effectively and for certain types of operations like passing large structures to functions without copying them.

A pointer in programming languages like Go is a type of variable that stores the memory address of another variable. To understand this concept, it helps to know a bit about how computer memory works:

  1. Memory and Addresses: Think of your computer's memory as a big set of drawers, each with a unique number (address). Every piece of data (like a variable) is stored in one of these drawers.

  2. Variables and Memory: When you create a variable, the computer stores it in one of these drawers and assigns an address to it. For example, if you create an integer variable a with a value of 10, the number 10 is stored in a drawer, and that drawer is given a unique address.

  3. What Pointers Do: A pointer is a special type of variable. Instead of holding a regular value like 10 or "hello", it holds the address of another drawer (i.e., another variable). This is like saying, “Don't remember the number 10; remember where I kept the number 10.”

  4. How Pointers are Used: You can use a pointer to read or modify the value stored in the address it points to. This is like going to the drawer whose number you remember and checking what's inside or putting something new in it.

Example using pointer in variables.

package main

import "fmt"

func main() {
    var a int = 10     // An integer variable
    var p *int         // A pointer variable, which will hold the address of an integer
    p = &a             // The address of 'a' is assigned to 'p'

    fmt.Println("Value of a:", a)   // The value of 'a' (10)
    fmt.Println("Address of a:", &a) // The memory address of 'a'
    fmt.Println("Value of p:", p)   // The value of 'p' (address of 'a')
    fmt.Println("Value at the address stored in p:", *p) // Dereferencing p to get the value of 'a' (10)
}

In the code var p *int = &a, you are creating a pointer in Go. Let's break it down into simpler terms:

  1. var p *int: This part declares a new variable named p. The *int tells you that p is not just a regular integer variable; it's a special kind of variable known as a pointer, and this particular pointer is meant to hold the address of an integer.

  2. = &a: This part assigns a value to p. The &a takes the address of the variable a. Think of & as saying "the location of". So &a means "the location in memory where a is stored".

Put together, var p *int = &a means:

  • You create a pointer variable p that is designed to hold the address of an integer.

  • You then set p to point to the location in memory where a is stored.

So after this line of code, p doesn’t hold an actual number like 42 but rather the address or the place in the computer's memory where the number 42 (the value of a) is stored. This allows you to access and manipulate the value of a indirectly through p.

Are Pointers really necessary in Programming?

Using a pointer to store the memory address of another variable might seem like it's wasting memory at first glance, but pointers are actually a powerful feature in many programming languages, including Go, for several reasons:

  1. Efficiency with Large Data: When you have large data structures (like big structs or arrays), copying them every time you pass them to functions can be very inefficient in terms of memory and performance. Pointers allow you to pass the address of the data instead, avoiding the need for copying.

  2. Mutability of Function Arguments: In Go, arguments are passed to functions by value, meaning they are copied. If you want a function to modify the original data, not just the copy, you need to pass a pointer to the data. This is essential for modifying structs, arrays, or slices within functions.

  3. Dynamic Data Structures: Pointers are crucial for implementing dynamic data structures like linked lists, trees, and graphs. These structures often require nodes that point to other nodes, and pointers provide a way to do this.

  4. Interface Implementation: In some cases, using pointers is necessary to implement interfaces. For instance, if a method has a pointer receiver, you must use a pointer to implement the interface.

  5. Memory Management Control: Pointers give you more control over memory management. You can allocate and deallocate memory dynamically, which can lead to more efficient use of memory.

  6. Polymorphism: Pointers, especially to interfaces, allow for polymorphism. You can write functions that take interface pointers and handle different types depending on the underlying concrete type.

Example using a Pointer in a struct

Imagine you have a large struct representing a complex data object. If you pass this struct to a function without using a pointer, the entire struct is copied, which can be inefficient. But if you pass a pointer, only the address is copied, which is much smaller and more efficient, especially for large structs.

type LargeStruct struct {
    // imagine lots of fields here
}

func process(data LargeStruct) {
    // processing data
}

func main() {
    largeData := LargeStruct{/* ... */}
    process(largeData)  // Inefficient copying of the entire struct
}

Changing process to take a pointer:

func process(data *LargeStruct) {
    // processing data using a pointer
}

Now, process takes a pointer, which is more efficient for large data structures.

while pointers do use additional memory to store an address, the memory and performance benefits they offer, especially with large data structures and in specific programming patterns, make them a valuable tool in a programmer's toolkit.

Pointers with respect to arrays and slices in Go are interesting because of how arrays and slices are treated differently in the language. Here's a basic explanation and some examples:

Pointers in Arrays and Slices

  • Arrays: In Go, an array is a fixed-size sequence of elements of a single type. When you pass an array to a function, Go copies the entire array, which can be inefficient for large arrays.

  • Pointers to Arrays: You can pass a pointer to an array to a function. This way, you're passing the address of the array, not the entire array, which is more efficient for large arrays.

Slices and Pointers

  • Slices: Slices are a more flexible and commonly used data type in Go. They are a reference type, meaning they already contain a pointer to an array. When you pass a slice to a function, you're passing the reference, not the entire collection of elements.

  • Pointers to Slices: Pointers to slices are less common because slices are already reference types. However, you might use a pointer to a slice if you need to modify the slice itself (its length, capacity, or even reassign it entirely) in a function.

Examples

Pointers and Arrays

package main

import "fmt"

func modifyArray(arr *[3]int) {
    arr[0] = 90
}

func main() {
    array := [3]int{10, 20, 30}
    modifyArray(&array)
    fmt.Println(array) // Will output [90 20 30]
}

In this example, modifyArray takes a pointer to an array of integers. We pass the address of array, so the function modifies the original array.

Slices

package main

import "fmt"

func modifySlice(slice []int) {
    slice[0] = 90
}

func main() {
    slice := []int{10, 20, 30}
    modifySlice(slice)
    fmt.Println(slice) // Will output [90 20 30]
}

Here, modifySlice takes a slice of integers. Since slices are reference types, when we pass slice to modifySlice, any modifications to the slice elements are reflected in the original slice.

Pointers to Slices

package main

import "fmt"

func reassignSlice(slice *[]int) {
    *slice = []int{1, 2, 3}
}

func main() {
    slice := []int{10, 20, 30}
    reassignSlice(&slice)
    fmt.Println(slice) // Will output [1 2 3]
}

In this last example, reassignSlice takes a pointer to a slice of integers. The function reassigns a new slice to the original slice variable, which is reflected outside the function.