A Comprehensive Guide to Pointers in Go

Jamal Kaksouri
10 min readFeb 26, 2023

--

A Comprehensive Guide to Pointers in Go

Introduction

Pointers are one of the most powerful and fundamental features of the Go programming language. They allow you to manipulate memory directly, create dynamic data structures, and improve program efficiency. However, pointers can also be tricky to use, and incorrect usage can lead to memory leaks and other errors. In this article, we will explore the basics of pointers in Go, including syntax, memory management, pass by reference, pointer arithmetic, arrays, and functions. We will also cover advanced concepts in pointers and best practices for using pointers in Go. By the end of this article, you should have a solid understanding of how to use pointers in Go and how to avoid common pitfalls.

Table of Contents:

  1. Pointers Basics
  2. Memory Management with Pointers
  3. Pass by Value vs. Pass by Reference
  4. Pointer Arithmetic
  5. Arrays and Pointers
  6. Functions and Pointers
  7. Advanced Concepts in Pointers
  8. Best Practices for Using Pointers in Go
  1. Pointers Basics

Let’s start with the basics. Pointers are a fundamental concept in computer science and are used in many programming languages, including Go. A pointer is a variable that stores the memory address of another variable. This allows you to manipulate the memory directly and create references to variables. A pointer is a variable that stores the memory address of another variable. In Go, pointers are represented by the * symbol. Here's an example:

var x int = 10
var ptr *int = &x

fmt.Println(x) // output: 10
fmt.Println(ptr) // output: 0xc0000140a8
fmt.Println(*ptr) // output: 10

In this example, we declare an integer variable x and initialize it with the value 10. We also declare a pointer variable ptr of type *int and initialize it with the memory address of x using the & operator. We then print the value of x, the memory address of x stored in ptr, and the value of x using the * operator, which dereferences the pointer and gives us the value stored at the memory address.

2. Memory Management with Pointers:

In Go, memory management is automatic and handled by the garbage collector. However, Go also provides the ability to allocate memory dynamically using pointers. Dynamic memory allocation allows you to create data structures that can grow and shrink as needed. Here’s an example:

var ptr *int = new(int)

fmt.Println(ptr) // output: 0xc0000160c0
fmt.Println(*ptr) // output: 0

*ptr = 10
fmt.Println(*ptr) // output: 10

ptr = nil

In this example, we declare a pointer variable ptr of type *int and use the new function to allocate memory for an integer value. We then print the memory address stored in ptr, which is the address of the newly allocated memory block. We also print the value of *ptr, which is the value stored at the memory address, which is initially set to 0. We then assign the value 10 to the memory location pointed to by ptr using the * operator. Finally, we set the pointer variable to nil, which frees the memory block allocated by new.

3. Pass by Value vs. Pass by Reference:

In Go, all function parameters are passed by value by default. This means that a copy of the parameter value is passed to the function. However, you can use pointers to pass parameters by reference, which means that the function can modify the original parameter value. Here’s an example:

func addOne(x *int) {
*x++
}

func main() {
x := 10
fmt.Println(x) // output: 10
addOne(&x)
fmt.Println(x) // output: 11
}

In this example, we declare a function addOne that takes a pointer parameter x of type *int. Inside the function, we dereference the pointer using the * operator and increment the value stored at the memory location pointed to by x. We then declare an integer variable x and initialize it with the value 10. We print the value of x, which is 10. We then call the addOne function and pass the memory address of x using the & operator. This allows the function to modify the original value of x by reference. Finally, we print the value of x, which is now 11.

4. Pointer Arithmetic:

Pointer arithmetic is the ability to perform arithmetic operations on pointers, such as addition and subtraction. In Go, pointer arithmetic is limited to a few use cases. For example, you can use pointer arithmetic to access elements of an array, but you cannot use it to perform arbitrary pointer arithmetic like in C/C++. Here’s an example:

func main() {
arr := [3]int{1, 2, 3}
ptr := &arr[0]
fmt.Println(*ptr) // output: 1
ptr++
fmt.Println(*ptr) // output: 2
}

Note: The above example results in an error due to the lack of support in the Go language. In the following, I will provide a solution to overcome this issue.

In this example, we declare an integer array arr with three elements and initialize it with values. We then declare a pointer variable ptr of type *int and initialize it with the memory address of the first element of the array using the & operator. We print the value of the first element of the array using the * operator and the pointer ptr, which is 1. We then increment the pointer ptr using the ++ operator, which moves the pointer to the next element of the array. We then print the value of the new pointer ptr, which is 2.

As I mentioned earlier In Go, pointer arithmetic is not supported directly like in some other programming languages. The ++ operator cannot be used with pointers in Go. to achieve a similar effect of incrementing the pointer, you can use the following alternative approach:
In Go, a better way to increment a pointer is by using the unsafe package. However, it's important to note that working with the unsafe package can lead to undefined behavior if not used carefully.

Here’s an example of how you can increment a pointer using the unsafe package:

import (
"fmt"
"unsafe"
)

func main() {
arr := [3]int{1, 2, 3}
ptr := &arr[0]
fmt.Println(*ptr) // output: 1

ptr = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(unsafe.Sizeof(arr[0]))))
fmt.Println(*ptr) // output: 2
}

Note: Please exercise caution when using the unsafe package, as it bypasses some of Go's safety mechanisms. It's generally recommended to avoid using unsafe unless absolutely necessary and to explore alternative approaches whenever possible.

5. Arrays and Pointers:

In Go, arrays are value types, which means that when you assign an array to a variable or pass it to a function, a copy of the entire array is made. However, you can use pointers to create a reference to an array, which allows you to modify the original array. Here’s an example:

func printArray(arr *[3]int) {
fmt.Println(*arr)
}

func main() {
arr := [3]int{1, 2, 3}
ptr := &arr
fmt.Println(*ptr) // output: [1 2 3]
printArray(ptr) // output: [1 2 3]
}

In this example, we declare an integer array arr with three elements and initialize it with values. We then declare a pointer variable ptr of type *[3]int and initialize it with the memory address of the array using the & operator. We print the value of the pointer ptr, which is the entire array. We then call a function printArray and pass the pointer ptr as a parameter. Inside the function, we dereference the pointer using the * operator and print the entire array.

6. Functions and Pointers:

Functions are one of the most common use cases for pointers in Go. You can use pointers to pass arguments to functions by reference, which allows the function to modify the original parameter value. You can also use pointers to return values from functions by reference, which allows the function to modify a variable outside its scope. Here are some examples:

func swap(x *int, y *int) {
temp := *x
*x = *y
*y = temp
}

func double(x *int) *int {
result := *x * 2
return &result
}

func main() {
x := 1
y := 2
fmt.Println(x, y) // output: 1 2
swap(&x, &y)
fmt.Println(x, y) // output: 2 1
ptr := double(&x)
fmt.Println(*ptr) // output: 4
}

In this example, we declare a function swap that takes two pointer parameters x and y of type *int. Inside the function, we declare a temporary variable temp, which is used to swap the values stored at the memory locations pointed to by x and y. We then declare a function double that takes a pointer parameter x of type *int and returns a pointer to an integer value. Inside the function, we double the value stored at the memory location pointed to by x and return a pointer to the result.

We then declare two integer variables x and y and initialize them with values. We print the values of x and y, which are 1 and 2, respectively. We then call the swap function and pass the memory addresses of x and y using the & operator. This allows the function to swap the original values of x and y by reference. We then print the values of x and y, which are now 2 and 1, respectively.

Finally, we call the double function and pass the memory address of x using the & operator. This allows the function to double the original value of x by reference and return a pointer to the result. We then dereference the pointer ptr using the * operator and print the result, which is 4.

7. Advanced Concepts in Pointers:

In addition to basic pointer manipulation, Go supports advanced concepts in pointers that can be used to optimize performance and reduce memory usage. These include:

Pointer Arithmetic: Go supports pointer arithmetic, which allows you to perform arithmetic operations on pointers. This can be useful for traversing arrays and other data structures efficiently. Here is an example:

var arr [5]int = [5]int{1, 2, 3, 4, 5}
var ptr *int = &arr[0]
for i := 0; i < len(arr); i++ {
fmt.Println(*ptr)
ptr++
}

In this example, we declare an array arr of length 5 and initialize it with some values. We then declare a pointer variable ptr and initialize it with the memory address of the first element of arr using the & operator. We then traverse the array using a for loop and print the value of each element using the dereferenced pointer variable ptr. We increment the pointer variable ptr by 1 in each iteration of the loop using the ++ operator, which points to the next element in the array.

Null Pointers: Go supports null pointers, which are pointers that do not point to any valid memory location. Null pointers are often used to indicate that a pointer is not initialized or has been deallocated. To declare a null pointer in Go, you can use the nil keyword. Here is an example:

var ptr *int = nil

In this example, we declare a null pointer variable ptr of type *int using the nil keyword. This pointer does not point to any valid memory location and cannot be dereferenced.

Void Pointers: Go supports void pointers, which are pointers that can point to any data type. Void pointers are often used in situations where the data type of a pointer is unknown or needs to be determined at runtime. To declare a void pointer in Go, you can use the unsafe.Pointer type. Here is an example:

var ptr unsafe.Pointer

In this example, we declare a void pointer variable ptr of type unsafe.Pointer. This pointer can point to any data type and can be cast to other pointer types using type assertions.

Function Pointers: Go supports function pointers, which are pointers that point to functions. Function pointers are often used in situations where a function needs to be passed as an argument to another function or stored in a data structure. To declare a function pointer in Go, you use the function signature followed by the * operator. Here is an example:

func add(x, y int) int {
return x + y
}

var ptr func(int, int) int = add

In this example, we declare a function add that takes two integers as arguments and returns their sum. We then declare a function pointer variable ptr that points to the add function using its signature. This pointer can be used to call the function directly or passed as an argument to other functions.

8. Best Practices for Using Pointers in Go:

While pointers can be a powerful tool in Go, they can also be a source of bugs and memory leaks if not used properly. Here are some best practices for using pointers in Go:

  1. Avoid using unsafe.Pointer unless absolutely necessary: unsafe.Pointer should only be used in situations where the data type of a pointer is unknown or needs to be determined at runtime. In most cases, you should use typed pointers to ensure type safety and reduce the risk of bugs.
  2. Avoid using pointer arithmetic unless necessary: Pointer arithmetic can be useful for traversing arrays and other data structures efficiently, but it can also be error-prone and difficult to debug. In most cases, you should use range loops or other high-level constructs to traverse data structures instead of using pointer arithmetic.
  3. Use null pointers to indicate uninitialized or deallocated memory: Null pointers are a useful tool for indicating that a pointer is not initialized or has been deallocated. You should always check for null pointers before dereferencing them to avoid crashes and undefined behavior.
  4. Use function pointers sparingly: While function pointers can be useful in certain situations, they can also make code harder to read and maintain. In most cases, it is better to use interfaces or other high-level constructs to pass functions as arguments to other functions.
  5. Use pointers to reduce memory usage: Pointers can be a useful tool for reducing memory usage in Go. By using pointers to store large data structures, you can avoid copying the entire data structure every time it is passed to a function or stored in a data structure.

Conclusion

Pointers are a powerful feature of Go that allow you to manipulate memory addresses and access data directly in memory. They are commonly used to create references to variables, pass arguments to functions by reference, and return values from functions by reference. Pointers can be used to optimize performance and reduce memory usage in certain situations, but they can also introduce bugs and errors if used improperly. It’s important to understand how pointers work and use them judiciously in your code.

happy coding :)

--

--

Jamal Kaksouri

A Software Engineer | Freelancer | Writer | Passionate about building scalable, performant apps and continuously learning