Pointers in Go
Nowadays, most developers don’t usually use pointers in their routine. But in this article, I will explain using the Go programming language, what are pointers, and how they can be used.
First of all, let’s start at the beginning
Each declared variable has space and one memory address. The majority times the addresses are represented by numbers like 0xc000025482, this means that they are represented by numbers in base 16, in other words, hexadecimal numbers.
If you want to discover the memory address of a variable in Go, you just need to write the code below:
package mainimport "fmt"func main() {
var example int fmt.Println(&example)
}
As you see, in the variable example, we set a & before print the value to the compiler get the address of it. The result will be something like 0xc000094010.
The RAM is basically a big block of sequential spaces. We declare a struct to reserved a part of the memory and each field of this struct is a sequential part of the block.
To you understand this easily, I created a struct that saves some values:
package mainimport "fmt"func main() {
var product struct {
id int64
name string
} fmt.Println(&product.id) // 0xc00000c0a0
fmt.Println(&product.name) // 0xc00000c0a8
}
If you run this code, the compiler will print the memory address of each struct property inside the RAM.
But, what is a pointer ?…
The concept of pointers is simple, a pointer is a variable that has your memory address and saves another memory address.
So different from other types of variables (int32, int64, string, bool, etc), pointers save hexadecimal address numbers.
But, how I declare a pointer? Let’s see below:
package mainimport "fmt"func main() {
var age int
var pointer *int fmt.Println(age) // 0
fmt.Println(pointer) // <nil>
}
In the code above, we declare two variables, one is the age that saves an integer value and another is a pointer that saves a memory address of an integer variable.
If you run this code, the compiler will print the value of each variable declared. How we don’t assign any value to variables, the Go set them as 0 and nil respectively.
The value nil in Go is like null in other languages. As we see, we declare the variable but don’t point to any other variable, so the compiler set nil and this means at moment the pointer is not pointing to any other value.
package mainimport "fmt"func main() {
var age int
var pointer *int age = 20
pointer = &age fmt.Println(age) // 20
fmt.Println(pointer) // 0xc00002c008
}
Now we are saying to the compiler that the variable pointer is pointing to the variable age, and when the value of the pointer is printed we have the memory address of the age variable.
But ok, what I can do with it?
Probably you are thinking “Ok, but is it just it?”, and the answer is no.
Now, as we see the pointer has access to the memory address of the variable, so is possible to have access to the value of this address.
Below I will show you how to do that in Go:
package mainimport "fmt"func main() {
var age int
var pointer *int age = 20
pointer = &age fmt.Println(age) // 20
fmt.Println(*pointer) // 20
}
Putting the * before the pointer, we are saying to the compiler search for the value of the memory address inside the pointer. So, we can do some interesting things with that. Let’s try to change the age value:
package mainimport "fmt"func main() {
var age int
var pointer *int age = 20
pointer = &age
*pointer = 25 fmt.Println(age) // 25
fmt.Println(*pointer) // 25
}
Why did it work? Basically we are saying to the compiler assign the number 25 to the value that the pointer is pointing. So now the variable age is equal a 25.
How pointers can be powerful?
Imagine that you have a list of 234.434.749.988 items and you want to remove just one item from the list.
The first thing that you can think is read item per item and when you find the item remove it. But, this is not the best solution and can make your system too slow.
Here is our system of tasks to exemplify:
package maintype Task struct {
title string
next *Task
}func main() {
t1 := Task{title: "Fix bug 1"}
t2 := Task{title: "Fix bug 2"}
t3 := Task{title: "Fix bug 3"} t1.next = &t2
t2.next = &t3 list := t1 fmt.Println(*list.next) // {Fix bug 2 0xc00010c000}
fmt.Println(*list.next.next) // {Fix bug 3 <nil>}
}
In this code, I created a struct called Task formed by a variable title, and another called next that points to another task.
After that, I declared three variables (t1, t2, t3). Each variable points to the next task and only t3 don’t because it is the last.
Finally, was declared a variable called list, assigned by t1. The struct of the list is like that:
Going back to our problem of removing an item from the list. By this struct, how we can remove task 2 (t2) without need read task per task?
What we can do to resolve this problem is very simple, we just need to point t1 to t3 and t2 to nothing. Therefore, t2 will be removed from the list because doesn’t point to any task and any task point to it.
Below I will show you how to do that:
package mainimport "fmt"type Task struct {
title string
next *Task
}func main() {
t1 := Task{title: "Fix bug 1"}
t2 := Task{title: "Fix bug 2"}
t3 := Task{title: "Fix bug 3"} t1.next = &t2
t2.next = &t3 list := t1
fmt.Println(*list.next) // {Fix bug 2 0xc00010c000} t1.next = &t3
t2.next = nil list = t1 fmt.Println(*list.next) // {Fix bug 3 <nil>}
}
Now the second task (t2) was removed and our list has just t1 and t3.
This is a very simple example. I will write an article explaining how to create a dynamic list in Go : )
Pass by reference and value. What is the difference?
The concept of pass by reference and pass by value is basically in all programming languages that work with functions.
The definition of pass by reference is the fact of pass a pointer that has reference to a variable on the memory to a function. And the definition of pass by value is the fact of pass directly a variable.
Let’s see two examples of it:
// PASS BY VALUEpackage mainimport "fmt"func main() {
var quantity int
fmt.Println(&quantity) // 0xc00002c008 quantity = double(quantity)
fmt.Println(&quantity) // 0xc00002c008
}func double(quantity int) int {
fmt.Println(&quantity) // 0xc00002c040
quantity = quantity * 2 return quantity
}
If you run the code, the compiler will print the sequence of addresses below:
0xc00002c008
0xc00002c040
0xc00002c008
What we need to pay attention here, is that when we called the double function and pass the quantity, Go creates another variable that has the same value of the quantity on the main function.
Because the variables quantity inside the main and double function belong to different contexts and have different memory addresses.
Now, let’s see an example of pass by reference:
// PASS BY REFERENCEpackage mainimport "fmt"func main() {
var pointer *int
var quantity int
pointer = &quantity
fmt.Println(pointer) // 0xc00002c008 pointer = double(pointer)
fmt.Println(pointer) // 0xc00002c008
}func double(value *int) *int {
fmt.Println(value) // 0xc00002c008
*value = *value * 2 return value
}
If you run the code, the compiler will print the sequence of addresses below:
0xc00002c008
0xc00002c008
0xc00002c008
Now all addresses are equal. Why?
Because what double function is receiving is a pointer to the variable quantity on the main function.
Go creates another variable too, but this variable is pointing to the same variable quantity. So the address keeps being the same and we didn’t duplicate the value of the quantity, saving memory space.
Should we always use pass by reference?
The answer to this question, as for all in computer science, is depends.
If we use pass by reference we can save more memory space than use pass by value. But what is happening with these pointers after the code was processed?
The pointers stay “loose” on the memory and we need a system called Garbage Collector to remove these pointers and free up memory space.
But the Garbage Collector requires processing time, and if we use it with a lot of frequency the system can be slow, damaging our application.
Conclusion
In the world of technology not exists silver bullets.
We as engineers must know how to deal with different scenarios and use the appropriate tools to do it.
For those who only use a hammer, every problem is a nail.
Pointers are a powerful technique to resolve some problems in our routine. So, is very interesting you understand some computer science concepts like that, to increase your vision about how to resolve a problem by the best way possible.
I hope that you had enjoyed it. Thanks!