A pointer is simply a variable that does not contain a value itself but rather a memory address where the final variable is located.
Using pointers allows us to access a set of data centrally without having to make a copy of it; if the variable in question is large, this can result in considerable RAM savings.
Imagine we have a variable and a pointer that points to that variable:
a := 3.1415
ptr = &a
This generates the following data layout in RAM:
RAM | Pointer |
---|---|
In addition to the above, we must consider that in Go, a function can receive a variable’s value in two different ways:
- By Value: A copy of the variable’s value is passed.
- By Reference: The memory address (pointer) where the variable is located is passed, making it a “shared” variable.
This article is divided into several sections for easier understanding:
- Pointer operations.
- Basic pointers.
- Pointers in functions.
- Pointers to arrays/slices.
- Pointers to structs.
- Pointers in method receiver parameters.
- Pointer arithmetic.
- Considerations.
If you are new to the world of Go, I recommend the following previous articles:
Pointer operations:
The following table indicates the operations that can be performed on pointers:
Action | Operator | Example |
---|---|---|
Get the memory address of a variable. | & | memoryAddress := &var |
Create a pointer. | * | Regular mode: var ptr *int = &i Sugar syntax: ptr := &i |
Modify the value of the variable using a pointer. | * | *ptr = 2 |
Basic pointers:
We can see how it is equivalent to work with a variable as well as work with a pointer to that variable:
package main
import "fmt"
func main() {
i := 1
fmt.Println("i original value:", i)
// Get pointer
iptr := &i
// Change final variable value
*iptr = 2
fmt.Println("i modified by pointer:", i)
fmt.Println("i read via pointer:", *iptr)
fmt.Println("Memory address i variable:", &i)
}
When executing the code, we get the following output:
i original value: 1
i modified by pointer: 2
i read via pointer: 2
Memory address i variable: 0x86c09e010
Pointers in functions:
If a function is going to receive an input variable that is a pointer, we define it as follows:
func zeroptr(iptr *int) {
In this simple example, we can see two functions, zeroval
which receives the value of the i
variable by copy, and the function zeroptr
which receives it by reference:
package main
import "fmt"
// Copy value function
func zeroval(ival int) {
ival = 0
}
// Pointer function
func zeroptr(iptr *int) {
*iptr = 0
}
func main() {
i := 1
fmt.Println("i original value:", i)
// Pass variable value copy to zeroval() function
zeroval(i)
fmt.Println("i zeroval value:", i)
// Get pointer
iptr := &i
// Pass pointer to zeroptr() function
zeroptr(iptr)
fmt.Println("i zeroptr value:", i)
fmt.Println("Memory address i variable:", &i)
}
When executing the code, we get the following output:
i original value: 1
i zeroval value: 1
i zeroptr value: 0
Memory address i variable: 0x86c09e010
We can see that the value of the i
variable in the main function was only affected when we passed the pointer, as this made it a “shared” variable between the main function and zeroptr
.
When passing variable values to functions by copy, we would have to overwrite the value of i
in the main function:
i = zeroval(i)
And when calling zeroval()
, return a value:
func zeroval(ival int) int {
ival = 0
return ival
}
Achieving the same result as using pointers, with the only difference being that we would be using a bit more RAM. The final code would look like this:
package main
import "fmt"
// Variable value continues being passed by value
// Yet now it returns an int value
func zeroval(ival int) int {
ival = 0
return ival
}
// Pointer function
func zeroptr(iptr *int) {
*iptr = 0
}
func main() {
i := 1
fmt.Println("i original value:", i)
// Pass variable value copy to zeroval() function
// Overwrite i value with zeroval() return value
i = zeroval(i)
fmt.Println("i zeroval value:", i)
// Get pointer
iptr := &i
// Pass pointer to zeroptr() function
zeroptr(iptr)
fmt.Println("i zeroptr value:", i)
fmt.Println("Memory address i variable:", &i)
}
When executing the code, we get the following output:
i original value: 1
i zeroval value: 0
i zeroptr value: 0
Memory address i variable: 0x86c09e010
Pointers to arrays/slices:
When modifying the values of an array/slice using pointers, it is not necessary to use *
as we have seen so far.
This can be done in two different ways:
- (*pointer)[N]
- pointer[N]
With an example, it will be clearer:
package main
import (
"fmt"
)
func modify(arr *[3]int) {
(*arr)[0] = 90
}
func modify2(arr *[3]int) {
arr[0] = 80
}
func main() {
a := [3]int{89, 90, 91}
fmt.Println("array value:", a)
aPtr := &a
modify(aPtr)
fmt.Println("array value:", a)
modify2(aPtr)
fmt.Println("array value:", a)
}
When executing the code, we get the following output:
array value: [89 90 91]
array value: [90 90 91]
array value: [80 90 91]
Remember that slices are directly pointers, so when we pass a slice to a function, we are passing the pointer:
package main
import (
"fmt"
)
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := []int{89, 90, 91}
fmt.Println("slice value:", a)
modify(a)
fmt.Println("slice value:", a)
}
When executing the code, we get the following output:
slice value: [89 90 91]
slice value: [90 90 91]
The idiomatic way to modify arrays using pointers is not by passing the pointer, but by receiving the array in the function and storing it in a slice, as this example clearly shows.
package main
import (
"fmt"
)
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
fmt.Println("array value:", a)
modify(a[:])
fmt.Println("array value:", a)
}
When executing the code, we get the following output:
array value: [89 90 91]
array value: [90 90 91]
Pointers to structs:
When modifying the values of a struct using pointers, the same thing happens as with arrays; it is not necessary to use *
.
This can be done in two different ways:
- (*pointer).myField
- pointer.myField
package main
import "fmt"
type employee struct {
name string
salary float64
}
// Set salary using (*pointer)
func setSalary1 (employeeToModify *employee, salary float64) {
(*employeeToModify).salary = salary
}
// Set salary using pointer directly
func setSalary
2 (employeeToModify *employee, salary float64) {
employeeToModify.salary = salary
}
func main() {
employeeOne := employee{
name: "John",
salary: 1000,
}
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Regular structure variable access
employeeOne.salary = 2000
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Get pointer
employeeOnePtr := &employeeOne
// Access structure variable value using (*pointer)
setSalary1(employeeOnePtr, 3000)
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Access structure variable value using pointer directly
setSalary2(employeeOnePtr, 4000)
fmt.Println("employeeOne Salary:", employeeOne.salary)
}
When executing the code, we get the following output:
employeeOne Salary: 1000
employeeOne Salary: 2000
employeeOne Salary: 3000
employeeOne Salary: 4000
Normally, when it comes to pointers to structs, we access their fields without using *()
as it is more convenient. However, if we generate a pointer, we can also access the methods of the final variable through it.
package main
import "fmt"
type employee struct {
name string
salary float64
}
// Show salary method
func (s employee) showSalary() float64 {
return s.salary
}
func main() {
employeeOne := employee{
name: "John",
salary: 1000,
}
fmt.Println("employeeOne Salary structure method:", employeeOne.showSalary())
// Get pointer
employeeOnePtr := &employeeOne
// Access structure variable value calling ptr method
fmt.Println("employeeOne Salary pointer method:", employeeOnePtr.showSalary())
}
When executing the code, we get the following output:
employeeOne Salary structure method: 1000
employeeOne Salary pointer method: 1000
Pointers in method receiver parameters:
When using methods associated with types, the same thing happens; moreover, it should be noted that even when methods require a pointer-type receiver parameter
, it is not necessary to create a pointer from which to call these methods.
In the following example, the value of the struct’s fields is altered using methods associated with the employee
type, where these methods require a pointer. However, as we can see, it is equally valid to call the methods from the original object as from its pointer:
employeeOne.setSalary1(3000)
employeeOnePtr.setSalary1(5000)
The complete code would look as follows:
package main
import "fmt"
type employee struct {
name string
salary float64
}
// Set salary using (*pointer)
func (s *employee) setSalary1 (salary float64) {
(*s).salary = salary
}
// Set salary using pointer directly
func (s *employee) setSalary2 (salary float64) {
s.salary = salary
}
func main() {
employeeOne := employee{
name: "John",
salary: 1000,
}
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Regular structure variable access
employeeOne.salary = 2000
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Access structure variable value using (*pointer)
employeeOne.setSalary1(3000)
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Access structure variable value using pointer directly
employeeOne.setSalary2(4000)
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Get pointer
employeeOnePtr := &employeeOne
// Access structure variable value using (*pointer), calling ptr method
employeeOnePtr.setSalary1(5000)
fmt.Println("employeeOne Salary:", employeeOne.salary)
// Access structure variable value using pointer directly, calling ptr method
employeeOnePtr.setSalary2(6000)
fmt.Println("employeeOne Salary:", employeeOne.salary)
}
When executing the code, we get the following output:
employeeOne Salary: 1000
employeeOne Salary: 2000
employeeOne Salary: 3000
employeeOne Salary: 4000
employeeOne Salary: 5000
employeeOne Salary: 6000
Pointer arithmetic:
Go does not support pointer arithmetic.
Safety. Without pointer arithmetic, itβs possible to create a language that can never derive an illegal address that succeeds incorrectly.
Compiler and hardware technology have advanced to the point where a loop using array indices can be as efficient as a loop using pointer arithmetic.
Also, the lack of pointer arithmetic can simplify the implementation of the garbage collector.
Considerations when working with pointers:
As a general rule, it is safer to work by copying values, even if we consume a bit more RAM. It is harder to cause problems if each function has its own copy, especially if our software is concurrent.
We should only use pointers if our structures are large enough to represent a significant RAM saving. In cases where the variables are very large, it is recommended to pass the structures by pointers even when the content of the structure will not be modified and only read from it.
Moreover, there is a small drawback in terms of the unpredictability of RAM usage if we use pointers, as when calling a function:
- The variables of that specific function are created in the
stack.
- If that function returns a pointer, the variable it points to will reside in the memory region assigned to that function, which is called the
frame.
- When returning from the function to
main,
theframe
is reused for any other use, leaving a pointer pointing to a region with invalid data. - To solve this, when returning from the function, Go automatically moves the variable to the
heap.
- The drawback of this is that the garbage collector will have to check every so often that there are no references to that variable to free up the RAM in the
heap
as soon as possible.
We can see an example in the following code:
package main
import "fmt"
func getIntPtr() *int {
i := 1
iptr := &i
return iptr
}
func main() {
iptr := getIntPtr()
fmt.Println("iptr value:", *iptr)
}
The variable i
in the getIntPtr()
function was first created in the stack
in the frame
of that function. When returning, Go automatically detected that there is a pointer pointing to that variable and migrated it to the heap.
Running the code, we can observe that accessing the value of i
through the pointer remains correct and has not been corrupted in any way:
iptr value: 1
RAM release is not immediate as it is when using values by copy. With pointers, it will be released when the garbage collector detects that it is possible to do so.
I highly recommend watching this video , which explains the functioning of pointers in Go very well.