A brief intro to Memory Addresses
Before you can understand Pointers, you first need to be familiar with memory addresses. Your OS uses memory addresses to track where all your data is stored in your system’s memory (RAM). Here’s an example of what a memory address looks like:
Note: To view a variable’s memory address, you need to prefix the variable with an ampersand
& (line 15). In this example, the memory address is 0xc00008e1e0, and it is where the string data, “Simon”, is stored. The OS has a look-up table that keeps track of which variable is mapped with which memory address. So to print out the variable (line 12), the OS first use the look-up table to find out what the corresponding memory address is for the variable, name; it then read the data that’s stored at that memory address (i.e. Simon) and returns it to the
The memory address itself can also tell you what type of data can be stored at that address. You can use the
reflect.TypeOf() (line 16) function to get this info, which in this example is
*string. The asterisk,
*, means that the memory address 0xc00008e1e0 points to some memory that’s allowed to only store
Now, if you save a memory address to a variable, and you want to tell Go to get the underlying data stored at that address, then you can do that by prefixing that variable with an asterisk:
Notice on line 16 I used to “*&”. That’s to show what happens. As you can see, they effectively cancel each other out.
What are Pointers?
Now that we’ve covered memory addresses let’s now turn our attention to pointers. A Pointer is a variable that’s used for storing memory addresses. Here’s an example:
Before I explain what’s going on above, let's visualise pointers as being made up of 2 boxes:
Line 10: We declare a new pointer variable. When creating a pointer variable, you also have to specify the type of object that this pointer variable will ultimately point to, which in this example is a string value.
Line 12: This shows that myPointer is a string pointer, i.e. it can store the memory address of a box that can store string data.
Line 14: This prints out the memory address of the pointer variable itself (memoryAddress1).
Line 16: This prints out the value stored in the myPointer variable. Since we have not provided a value, it meant that initialized the pointer with its zero value. The zero value for pointers is
<nil>. This means memoryAddress2 is nil, i.e. Box 2 itself doesn’t exist yet.
To create box 2, you need to generate memoryAddress2, and for strings, that can be done using the built-in
new() function. The
new() function creates a box that can hold the specified data type and returns the memory address of that box:
In the above example, we’re just printing the memory address to the terminal. So let’s use this approach to create box 2 for our pointer:
Here’s what this looks like visually:
Line 13: We use the
*varName to store data at box 2’s memory address.
Line 15: The pointer’s own memory address
0xc000010200, isn’t something that we care about that much, but I have included it in the above examples so that you are aware it exists and how to tell it apart from the memory address that we are actually interested in,
Pointers versus Regular Variables
There are other ways to create box 2. For example, if you have an existing regular variable, then you can create a pointer variable and link it to that box:
Notice how you can now access the variable directly using the variable name (line 18) or via the pointer (line 19).
Going back to how a pointer looks visually:
By contrast, a regular variable is a lot simpler:
With regular variables, you don’t have to worry about memory addresses, asterisks, and asterisks. Instead, you use the variable name to access and update your variable’s value. So if regular variables are easier to understand and use, then what’s the point of Pointers (pun intended 😉)? We’ll cover that next.
Why use Pointers?
Pointers can let functions update outside variables. For example:
On line 16, we didn’t pass in “40000” into the payRise() function; instead, we passed in the memory address (aka pointer) of where the “40000” value is stored, and the payRise() function did update the data in that location.
Now let’s take a look at an equivalent example, but this time using regular variables.
This time we had to set up an output parameter for our payRise() function (line 7) to return the updated value (line 9), which we then capture into our salary variable (line 16).
Just before line 8 is executed in this approach, Go declared a new variable behind the scenes, called pay. Unfortunately, this variable ends up storing the same data as what’s stored in the variable salary. Thus, this approach (aka pass-by-value) results in duplicate data stored in system memory.
This isn’t a big problem when the data in question is of a primitive datatype, e.g. a string, integer, boolean,…etc. However, if the variable is something bigger (composite data type), e.g. a Struct, it can cause performance issues and will cause inefficient use of memory. That’s why in those scenarios, it’s a lot better to use Pointer.
Another advantage with Pointers is that it can also hold
nil values. So if a certain data type doesn’t allow nil values, e.g. a struct, and you want to set it to nil, you can use a struct pointer to achieve that.