Value versus pointer
With values such as int
, bool
, and string
, when you pass them to a function, Go makes a copy of the value, and it’s the copy that’s used in the function. This copying means that a change that’s made to the value in the function doesn’t affect the value that you used when calling the function.
Passing values by copying tends to result in code that has fewer bugs. With this method of passing values, Go can use its simple memory management system, called the stack. The downside is that copying uses up more and more memory as values get passed from function to function. In real-world code, functions tend to be small, and values get passed to lots of functions, so copying by value can sometimes end up using much more memory than is needed.
There is an alternative to copying that uses less memory. Instead of passing a value, we create something called a pointer and then pass that to functions. A pointer is not a value itself, and you can’t do anything useful with a pointer other than getting a value using it. You can think of a pointer as the address of the value you want, and to get to the value, you must go to the address. If you use a pointer, Go won’t make a copy of the value when passing a pointer to a function.
When creating a pointer to a value, Go can’t manage the value’s memory using the stack. This is because the stack relies on simple scope logic to know when it can reclaim the memory that’s used by a value, and having a pointer to a variable means these rules don’t work. Instead, Go puts the value on the heap. The heap allows the value to exist until no part of your software has a pointer to it anymore. Go reclaims these values in what it calls its garbage collection process. This process happens periodically in the background, and you don’t need to worry about it.
Having a pointer to a value means that a value is put on the heap, but that’s not the only reason that happens. Working out whether a value needs to be put on the heap is called escape analysis. There are times when a value with no pointers is put on the heap, and it’s not always clear why.
You have no direct control over whether a value is put on the stack or the heap. Memory management is not part of Go’s language specification. Memory management is considered an internal implementation detail. This means it could be changed at any time, and that what we’ve spoken about are only general guidelines and not fixed rules and could change at a later date.
While the benefits of using a pointer over a value that gets passed to lots of functions are clear for memory usage, it’s not so clear for CPU usage. When a value gets copied, Go needs CPU cycles to get that memory and then release it later. Using a pointer avoids this CPU usage when passing it to a function. On the other hand, having a value on the heap means that it then needs to be managed by the complex garbage collection process. This process can become a CPU bottleneck in certain situations – for example, if there are lots of values on the heap. When this happens, the garbage collector has to do lots of checking, which uses up CPU cycles. There is no correct answer here, and the best approach is the classic performance optimization one. First, don’t prematurely optimize. When you do have a performance problem, measure before you make a change, and then measure after you’ve made a change.
Beyond performance, you can use pointers to change your code’s design. Sometimes, using pointers allows for a cleaner interface and simplifies your code. For example, if you need to know whether a value is present or not, a non-pointer value always has at least its zero value, which could be valid in your logic. You can use a pointer to allow for an is not set
state as well as holding a value. This is because pointers, as well as holding the address to a value, can also be nil
, which means there is no value. In Go, nil
is a special type that represents something not having a value.
The ability for a pointer to be nil also means that it’s possible to get the value of a pointer when it doesn’t have a value associated with it, which means you’ll get a runtime error. To prevent runtime errors, you can compare a pointer to nil
before trying to get its value. This looks like <pointer> != nil
. You can compare pointers with other pointers of the same type, but they only result in true if you are comparing a pointer to itself. No comparison of the associated values gets made.
Pointers are powerful tools in the language thanks to their efficiency, ability to pass by reference (instead of pass by value) to allow functions to modify the original values, and how they allow for dynamic memory allocation using the garbage collector. However, with any great tool comes great responsibility. Pointers can be dangerous if misused, such as in the event memory is freed (deallocated) and the pointer becomes a “dangling pointer,” which could lead to undefined behavior if accessed. There is also the potential for memory leaks, unsafe operations due to direct memory access, and concurrency challenges if there are shared pointers that could introduce data races. Overall, Go’s pointers are generally straightforward and less error-prone compared to other languages such as C.
Getting a pointer
To get a pointer, you have a few options. You can declare a variable as being a pointer type using a var
statement. You can do this by adding *
at the front of most types. This notation looks like var <name> *<type>
. The initial value of a variable that uses this method is nil
. You can use the built-in new
function for this. This function is intended to be used to get some memory for a type and return a pointer to that address. The notation looks like <name> := new(<type>)
. The new
function can be used with var
too. You can also get a pointer from an existing variable using &, which you can read as "address of"
. This looks like <var1> := &<
var2>.
Exercise 1.13 – getting a pointer
In this exercise, we’ll use each of the methods we can use to get a pointer variable. Then, we’ll print them to the console using fmt.Printf
to see what their types and value are. Let’s get started:
- Create a new folder and add a
main.go
file to it. - In
main.go
, add themain
package name to the top of the file:package main
- Import the packages we’ll need:
import ( "fmt" "time" )
- Create the
main()
function:func main() {
- Declare a pointer using a
var
statement:var count1 *int
- Create a variable using
new
:count2 := new(int)
- You can’t take the address of a literal number. Create a temporary variable to hold a number:
countTemp := 5
- Using
&
, create a pointer from the existing variable:count3 := &countTemp
- It’s possible to create a pointer from some types without a temporary variable. Here, we’re using our trusty
time
struct:t := &time.Time{}
- Print each out using
fmt.Printf
:fmt.Printf("count1: %#v\n", count1) fmt.Printf("count2: %#v\n", count2) fmt.Printf("count3: %#v\n", count3) fmt.Printf("time : %#v\n", t)
- Close the
main()
function:}
- Save the file. Then, in the new folder, run the following:
go run .
The following is the output:
Figure 1.19: Output showing pointers
In this exercise, we looked at three different ways of creating a pointer. Each one is useful, depending on what your code needs. With the var
statement, the pointer has a value of nil
, while the others already have a value address associated with them. For the time
variable, we can see the value, but we can tell it’s a pointer because its output starts with &
.
Next, we’ll see how we can get a value from a pointer.
Getting a value from a pointer
In the previous exercise, when we printed out the pointer variables for the int
pointers to the console, we either got nil
or saw a memory address. To get to the value a pointer is associated with, you must dereference the value using *
in front of the variable name. This looks like fmt.Println(*<val>)
.
Dereferencing a zero or nil
pointer is a common bug in Go software as the compiler can’t warn you about it, and it happens when the app is running. Therefore, it’s always best practice to check that a pointer is not nil
before dereferencing it unless you are certain it’s not nil
.
You don’t always need to dereference – for example, when a property or function is on a struct. Don’t worry too much about when you shouldn’t be dereferencing as Go gives you clear errors regarding when you can and can’t dereference a value.
Exercise 1.14 – getting a value from a pointer
In this exercise, we’ll update our previous exercise to dereference the values from the pointers. We’ll also add nil
checks to prevent us from getting any errors. Let’s get started:
- Create a new folder and add a
main.go
file to it. - In
main.go
, add themain
package name to the top of the file:package main
- Import the packages we’ll need:
import ( "fmt" "time" )
- Create the
main()
function:func main() {
- Our pointers are declared in the same way as they were previously:
var count1 *int count2 := new(int) countTemp := 5 count3 := &countTemp t := &time.Time{}
- For counts 1, 2, and 3, we need to add a
nil
check and add*
in front of the variable name:if count1 != nil { fmt.Printf("count1: %#v\n", *count1) } if count2 != nil { fmt.Printf("count2: %#v\n", *count2) } if count3 != nil { fmt.Printf("count3: %#v\n", *count3) }
- We’ll also add a
nil
check for ourtime
variable:if t != nil {
- We’ll dereference the variable using
*
, just like we did with thecount
variables:fmt.Printf("time : %#v\n", *t)
- Here, we’re calling a function on our
time
variable. This time, we don’t need to dereference it:fmt.Printf("time : %#v\n", t.String())
- Close the
nil
check:}
- Close the
main()
function:}
- Save the file. Then, in the new folder, run the following:
go run .
The following is the output:
Figure 1.20: Output showing getting values from pointers
In this exercise, we used dereferencing to get the values from our pointers. We also used nil
checks to prevent dereferencing errors. From the output of this exercise, we can see that count1
was a nil
value and that we’d have gotten an error if we tried to dereference. count2
was created using new
, and its value is a zero value for its type. count3
also had a value that matched the value of the variable we got the pointer from. With our time
variable, we were able to dereference the whole struct, which is why our output doesn’t start with &
.
Next, we’ll look at how using a pointer allows us to change the design of our code.
Function design with pointers
We’ll cover functions in more detail later in this book, but you know enough from what we’ve done so far to see how using a pointer can change how you use a function. A function must be coded to accept pointers, and it’s not something that you can choose whether to do or not. If you have a pointer variable or have passed a pointer of a variable to a function, any changes that are made to the value of the variable in the function also affect the value of the variable outside of the function.
Exercise 1.15 – function design with pointers
In this exercise, we’ll create two functions: one that accepts a number by value, adds 5 to it, and then prints the number to the console; and another function that accepts a number as a pointer, adds 5 to it, and then prints the number out. We’ll also print the number out after calling each function to assess what effect it has on the variable that was passed to the function. Let’s get started:
- Create a new folder and add a
main.go
file to it. - In
main.go
, add themain
package name to the top of the file:package main
- Import the packages we’ll need:
import "fmt"
- Create a function that takes an
int
pointer as an argument:func add5Value(count int) {
- Add
5
to the passed number:count += 5
- Print the updated number to the console:
fmt.Println("add5Value :", count)
- Close the function:
}
- Create another function that takes an
int
pointer:func add5Point(count *int) {
- Dereference the value and add
5
to it:*count += 5
- Print out the updated value of
count
and dereference it:fmt.Println("add5Point :", *count)
- Close the function:
}
- Create the
main()
function:func main() {
- Declare an
int
variable:var count int
- Call the first function with the variable:
add5Value(count)
- Print the current value of the variable:
fmt.Println("add5Value post:", count)
- Call the second function. This time, you’ll need to use
&
to pass a pointer to the variable:add5Point(&count)
- Print the current value of the variable:
fmt.Println("add5Point post:", count)
- Close the
main()
function:}
- Save the file. Then, in the new folder, run the following:
go run .
The following is the output:
Figure 1.21: Output displaying the current value of the variable
In this exercise, we showed you how passing values by a pointer can affect the value variables that are passed to them. We saw that, when passing by value, the changes you make to the value in a function do not affect the value of the variable that’s passed to the function, while passing a pointer to a value does change the value of the variable passed to the function.
You can use this fact to overcome awkward design problems and sometimes simplify the design of your code. Passing values by a pointer has traditionally been shown to be more error-prone, so use this design sparingly. It’s also common to use pointers in functions to create more efficient code, which Go’s standard library does a lot.
Activity 1.02 – pointer value swap
In this activity, your job is to finish some code a co-worker started. Here, we have some unfinished code for you to complete. Your task is to fill in the missing code, where the comments are to swap the values of a
and b
. The swap
function only accepts pointers and doesn’t return anything:
package main import "fmt" func main() { a, b := 5, 10 // call swap here fmt.Println(a == 10, b == 5) } func swap(a *int, b *int) { // swap the values here }
Follow these steps:
- Call the
swap
function, ensuring you are passing a pointer. - In the
swap
function, assign the values to the other pointer, ensuring you dereference the values.
The following is the expected output:
true true
Next, we’ll look at how we can create variables with a fixed value.