Before we begin talking about pointers, let’s learn a thing or two about hexadecimal
numbers. A hexadecimal number is a number with base 16
. If you are a web developer, then you are using them for a long time, because mostly; colors are represented in hex format. For example, white is represented as #FFFFFF
and black as #000000
.
In Go, you can save a hexadecimal number in a variable and Go provides a literal expression for that. If a number starts with 0x
then it’s a hexadecimal number.
package main
import "fmt"
func main() {
a := 0x00
b := 0x0A
c := 0xFF
fmt.Printf("variable a of type %T with value %v in hex is %X\n", a, a, a)
fmt.Printf("variable b of type %T with value %v in hex is %X\n", b, b, b)
fmt.Printf("variable c of type %T with value %v in hex is %X\n", c, c, c)
}
// variable a of type int with value 0 in hex is 0
// variable b of type int with value 10 in hex is A
// variable c of type int with value 255 in hex is FF
From the above example, we can see that values represented in the hexadecimal system are saved in the decimal system with data type int
.
But why we are learning about hexadecimal numbers when we are talking about pointers. Well, let’s talk about the memory address first.
When you are declaring a variable and providing some value (data), the Go runtime will allocate some memory for the value in the RAM and depending on data type, it will allocate a specific size of memory to store that value.
That memory will have some memory address (like a postal address) so that Go can find that variable’s value when asked for it. These memory addresses are represented in hexadecimal values.
How to access the memory address of a variable?
To access the address value (data) represented by a variable, Go provides the &
(ampersand) operator which is used in front of the variable name. By doing this, &variable_name
expression returns the memory address of the value (data) referenced by variable_name
variable.
package main
import "fmt"
func main() {
a := 0x00
b := 0x0A
c := 0xFF
fmt.Println("&a =", &a)
fmt.Println("&b =", &b)
fmt.Println("&c =", &c)
}
// &a = 0xc00012a010
// &b = 0xc00012a018
// &c = 0xc00012a020
We saw this in slice
lesson when we were trying to prove that two slices can reference values from the same array. In the above example, using &
operator, we found the memory address of the variable a
, b
and c
.
What is a pointer?
A pointer is a variable that points to the memory location of another variable (actually to the value referenced by the variable).
As we saw earlier, we can save a hexadecimal value to a variable, but under the hood, it is saved as a decimal value of type int
. But there is a catch.
Even though you could save a memory address (hexadecimal number) in a variable, it’s not a pointer or it does not point to the memory location of another variable. It is just a value and it has no idea what that value means.
That’s where pointer comes into play. A pointer is just a variable but of special kind and special data type.
A pointer also saves the memory address but it knows where that memory is located in the RAM and how to retrieve the value stored in that memory address. It can perform various kinds of operations with it like it can read the value stored at the memory address or write a new value.
Unlike saving hex value in a variable which has type int
, the pointer has the data type of *int
if it points to the memory address of int
data and *string
if it points to the memory address of string
data.
Syntax to create or define a pointer is var p *Type
where Type
is a data type the value (data) it will be pointing to. Let’s create a simple pointer.
package main
import "fmt"
func main() {
var pa *int
fmt.Printf("pointer pa of type %T with value %v\n", pa, pa)
}
// pointer pa of type *int with value <nil>
In the above example, we have created pointer pa
which points to the data of type int
but since we are not assigning any initial value to it, its zero-value is nil
. A pointer has a nil
value because it is not pointing to any data (value) in the RAM at the moment.
So let’s create a variable of type int
and make pa
point to it.
package main
import "fmt"
func main() {
a := 1
var pa *int
pa = &a
fmt.Printf("pointer pa of type %T with value %v\n", pa, pa)
}
// pointer pa of type *int with value 0xc000012028
In the above example, we have created a variable a
and assigned an initial value of 1
. Go will save an integer 1
somewhere in the RAM. Then we have created the pointer pa
which can point to int
value.
Later, we have assigned the memory address of the variable a
(its value actually) to the pointer pa
using pa = &a
expression. The above program can also be written with a shorthand variable assignment format.
package main
import "fmt"
func main() {
a := 1
pa := &a
fmt.Printf("pointer pa of type %T with value %v\n", pa, pa)
}
// pointer pa of type *int with value 0xc0000aa010
In the shorthand format, Go will interpret that we are trying to create a pointer because we are assigning the memory address of a variable (using the &
operator) to the variable we are trying to create.
When print the value of pa
, it returns the memory address it is pointing. Also, the data type of pa
is *int
which means that it is a pointer that points to the data of type int
.
However, it does not explicitly mention which variable or data it points to. But it can find out the data in that memory address.
Dereferencing a Pointer
To find out the value (data) a pointer it points to, we need to use *
operator, also called dereferencing operator which if placed before a pointer variable (like &
operator to get memory address), it returns the data in that memory.
package main
import "fmt"
func main() {
a := 1
pa := &a
fmt.Printf("data at %v is %v\n", pa, *pa)
}
// data at 0xc000012028 is 1
Changing the variable value using a Pointer
As we saw in the previous example, we can read the data at the memory location a pointer points to, but we can also change (write) the value at that memory location.
package main
import "fmt"
func main() {
a := 1
pa := &a
*pa = 2
fmt.Printf("a = %v\n", a)
fmt.Printf("data at %v is %v\n", pa, *pa)
}
// a = 2
// data at 0xc0000aa010 is 2
As you can see from the above example, *pa
syntax reads the value from the memory location pointed by the pointer, but the same expression can be used to write a new value at the same memory location.
If you are wondering, why the value of the variable a
changed. This is because a new value has been written at the memory address referenced by the variable a
. This proves that pointers are pretty powerful.
💡 The difference between a variable and pointer is that a variable stores the value at a memory address and pointer points to a memory address.
The new
function
Go provides the new
built-in function which allocates memory and returns a pointer to that memory. The syntax of new
function is as follows.
func new(Type) *Type
The first argument of the new
function is the data type and the returned value of this function is the pointer of that data type. This function will allocate some memory, write a zero-value of the Type
at that memory location and return a pointer to that memory location.
package main
import "fmt"
func main() {
pa := new(int)
fmt.Printf("data at %v is %v\n", pa, *pa)
}
// data at 0xc000124010 is 0
Did you expect the value (data) at memory location returned by the new
function to be nil
? Well, the zero-value of a pointer is nil
which means pointer is not pointing to any memory but when the pointer points it to a memory location, memory cannot be empty, it must hold some data.
Go stores zero-value of data type passed to the new
function and returns the memory address of it. Therefore, if you are interested in a pointer only, then you can use the new
function instead of creating a new variable and then a pointer which points to the value of the variable (as we saw earlier).
💡 Hence the definition “A pointer is a variable that points the memory address of another variable” is strictly not true. “A pointer is a variable which points a memory address” is more accurate.
Passing a Pointer to a Function
Like a variable, you can pass a pointer to a function. There are two ways you can do this. Either create a pointer and then pass it to the function or just pass an address of a variable.
package main
import "fmt"
func changeValue(p *int) {
*p = 2
}
func main() {
a := 1
pa := &a
changeValue(pa)
fmt.Printf("a = %v\n", a)
}
// a = 2
In the above program, we passed the pointer pa
as an argument to the changeValue
function. This pointer points to the value of the variable a
. Hence inside the function, we can write a new value at the memory address pointed by the pointer pa
which also mutates the value of the variable a
.
Instead of taking this long approach, we can shorten the above example by passing the address of the variable a
as an argument instead. The changeValue
function parameter p
is now the pointer.
💡 Two pointers pointing to the same value are the same.
package main
import "fmt"
func changeValue(p *int) {
*p = 2
}
func main() {
a := 1
changeValue(&a)
fmt.Printf("a = %v\n", a)
}
// a = 2
In the above program, the argument syntax of the changeValue
function instructs Go that we are expecting a pointer, especially the *int
part (type declaration) of the argument p
.
You can pass a pointer of a composite data type like array
to function.
package main
import "fmt"
func changeValue(p *[3]int) {
//*p == original array `a`
// *p[0] != (*p)[0]
(*p)[0] *= 2
(*p)[1] *= 3
(*p)[2] *= 4
}
func main() {
a := [3]int{1, 2, 3}
changeValue(&a)
fmt.Printf("a = %v\n", a)
}
// a = [2 6 12]
We could also write the above program using shorthand syntax provided by Go to access data from an array pointer.
package main
import "fmt"
func changeValue(p *[3]int) {
// (*p)[0] == p[0]
p[0] *= 2
p[1] *= 3
p[2] *= 4
}
func main() {
a := [3]int{1, 2, 3}
changeValue(&a)
fmt.Printf("a = %v\n", a)
}
// a = [2 6 12]
But passing array pointer as function parameter is not idiomatic to Go. We should prefer slices instead for this functionality. As we saw in the slices lesson, we can pass a slice as an argument to a function and that function can mutate the values inside the slice.
Pointer arithmetic
Unlike C
language, where a pointer can be incremented or decremented, Go does not allow pointer arithmetic.