Golang Basics - Pointers

What are pointers?

Every variable we declare is stored in a particular location in memory.

Variables and their locations in memory

Pointers are used to represent those memory locations of variables, instead of the variables themselves.

Pointers and their locations in memory

Pointers are variables as well, but their value is the memory address of another variable.

Declaring pointers

Pointers are declared just like a regular variable type. The only difference is that we use the * symbol, along with the type of variable the pointer is referencing to form the pointers type:

// Pointer types are defined using the `*` symbol
var wheelPtr *int
var modelNamePtr *string
var vehiclePtr *Vehicle

type Vehicle struct{
        Model string
        Wheels int
}

wheels := 2
modelName := "Ducati"
vehicle := Vehicle{
        Model: modelName,
        Wheels: wheels,
}

// We can get the address of variables using the `&` symbol
wheelPtr = &wheels
modelNamePtr = &modelName
vehiclePtr = &vehicle

When we declare a type *Vehicle, it refers to a variable which is supposed to be a pointer to a Vehicle type.

You can think of the & symbol to mean "address-of". So when we declare wheelPtr := &wheels, we mean: wheelPtr is assigned the address of the wheels variable. In the case of struct types, we can use a shorthand to declare its pointer:

vehiclePtr := &Vehicle{
        Model: modelName,
        Wheels: wheels,
}

Dereferencing pointers

To get the value that the pointer is pointing to, we need to dereference it.

Dereferencing means obtaining the actual value that the pointer is pointing at

// We can use the `*` symbol to dereference a pointer type variable
wheelValue := *wheelPtr
// wheelValue: 2

// We can access the fields of a struct via its pointer as well
vehicleModel := vehiclePtr.Model
// even though `vehiclePtr` is a pointer to the `Vehicle` struct, we can still access its fields directly

Default value of a pointer

All pointers default to nil. If we declare a pointer to any type of variable, it will be a nil-pointer until a value is assigned to it:

var ptr *int

fmt.Println(ptr)
// nil

If we try to dereference a nil pointer, we will get a runtime exception, and the program will fail:

var ptr *int

fmt.Println(*ptr)
// Output:
// panic: runtime error: invalid memory address or nil pointer dereference

Why do we use pointers?

In Go, everything is passed by value. Consider this piece of code:

var vehicle1 Vehicle
var vehicle2 Vehicle
var vehicle3 Vehicle

vehicle1 = Vehicle{
        Model: modelName,
        Wheels: wheels,
}

vehicle2 = vehicle1
vehicle3 = vehicle2

When we assign the value of vehicle1 to vehicle2 and vehicle2 to vehicle3, the entire Vehicle data structure gets copied over. Now, we have three copies of the vehicle currently in use. For smaller programs, this is barely noticeable. When you get to more complex and data intensive programs however, passing large data structures around can cause a huge overhead in terms of memory and computing power.

Pointers, don't suffer from this problem:

var vehicle1 *Vehicle
var vehicle2 *Vehicle
var vehicle3 *Vehicle

vehicle1 = &Vehicle{
        Model: modelName,
        Wheels: wheels,
}

vehicle2 = vehicle1
vehicle3 = vehicle2

We are still copying over value here. The difference now, is that the value that we're copying is just the pointer, which takes the form of an unsigned integer (as discussed in the post on types). This is much more memory efficient than the first method.

Modifying pointer values

One caveat while using pointers, is that if you modify the value of the pointer reference, other pointers pointing to the same value will show modified references as well:

var vehicle1 *Vehicle
var vehicle2 *Vehicle

vehicle1 = &Vehicle{
        Model: modelName,
        Wheels: wheels,
}

vehicle2 = vehicle1

vehicle2.Model = "BMW"

fmt.Println(vehicle1.Model)
// "BMW"

For most cases, using pointers is usually considered a better tradeoff for performance as compared to values.

📖 Read next: Maps


Liked this article? Share it on: