Go’s popularity has exploded in recent years. The 2020 HackerEarth Developer Survey found that Go was the most popular programming language among experienced developers and students. The 2021 Stack Overflow Developer Survey reported similar results, with Go being one of the top four languages developers want to use.
Given its popularity, it’s important for web developers to master Go, and perhaps one of the most critical components of Go is a pointer to it. This article explains the different ways to create Pointers and the types of problems they solve.
What is Go?
Go is a statically typed compiled language made by Google. There are many reasons why Go is a popular choice for building robust, reliable, and efficient software. One of the biggest attractions is the simplicity of Go’s approach to writing software, which is evident in the implementation of Pointers in the language.
Pass parameters in Go
When writing software in any language, developers must consider which code will mutate in their code base.
As you start composing functions and methods and passing all the different types of data structures in your code, you need to be careful about which should be passed as values and which as references.
Passing parameters by value is like passing a printed copy of something. If the owner of the copy scribbles on it or destroys it, nothing will change with the original copy you own.
Passing by reference is like sharing an original copy with someone else. If they change something, you can see — and have to deal with — the changes they’ve made.
Let’s start with a very basic piece of code and see if you can see why it might not do what we expect.
package main
import (
"fmt"
)
func main() {
number := 0
add10(number)
fmt.Println(number) // Logs 0
}
func add10(number int) {
number = number + 10
}
Copy the code
In the example above, I tried to get the add10() function to increment number 10, but it didn’t seem to work. It just returns 0. This is the problem that Pointers solve.
Use Pointers in Go
If we want the first snippet to work, we can use Pointers.
In Go, each function parameter is passed by value, which means that the value is copied and passed without any change in the underlying variable by changing the parameter value in the function body.
The only exceptions to this rule are slices and maps. They can be passed by value, and since they are reference types, any changes to where they are passed will change the underlying variables.
The way to pass arguments to functions that other languages consider “by reference” is to use Pointers.
Let’s fix our first example and explain what happened.
package main
import (
"fmt"
)
func main() {
number := 0
add10(&number)
fmt.Println(number) // 10! Aha! It worked!
}
func add10(number *int) {
*number = *number + 10
}
Copy the code
Syntax for addressing Pointers
The only major difference between the first snippet and the second snippet is the use of * and &. The operations that these two operators perform are called dereference/direction (*) and reference/memory address retrieval (&).
Reference and memory address retrieval use&
If you follow the snippet from main, the first operator we change is the ampere-symbol & before the number argument we pass to add10.
This gives us the memory address where we store variables in the CPU. If you add a log to the first snippet, you will see a memory address in hexadecimal notation. It will look like this: 0xC000018030 (which changes each time it is recorded).
This slightly cryptic string essentially points to the address where the variable is stored on the CPU. This is how Go shares variable references, so that all other places that have access to Pointers or memory addresses can see the change.
Dereference memory*
If we only have one memory address now, incrementing 10 to 0xC000018030 May not be what we need. That’s where dereference memory comes in.
We can use Pointers to defer the memory address to the variable it points to, and then evaluate it. We can see this in the code snippet in line 14 above.
*number = *number + 10
Copy the code
Here, we dereference our memory address to 0 and then 10.
Now, the code example should work as originally expected. Instead of copying values to reflect change, we share a single variable.
There are some extensions to the mental model we have created that will help further understand Pointers.
Used in Gonil
Pointer to the
Everything in Go is given a value of 0 the first time it is initialized.
For example, when you create a string, it defaults to an empty string (“”) unless you assign something to it.
Here are all the zeros.
0
Applies to all ints0.0
Applies to float32, Float64, complex64, and complex128false
Apply to bool""
Applies to stringsnil
For interfaces, fragments, channels, maps, Pointers, and functions
This is the same for Pointers. If you create a pointer, but you don’t point it to any memory address, it’s going to be nil.
package main
import (
"fmt"
)
func main() {
var pointer *string
fmt.Println(pointer) // <nil>
}
Copy the code
Use and dereference Pointers
package main
import (
"fmt"
)
func main() {
var ageOfSon = 10
var levelInGame = &ageOfSon
var decade = &levelInGame
ageOfSon = 11
fmt.Println(ageOfSon)
fmt.Println(*levelInGame)
fmt.Println(**decade)
}
Copy the code
As you can see here, we’re trying to reuse the ageOfSon variable in many places in our code, so we can always point things to other Pointers.
But in line 15, we must first dereference one pointer and then the next pointer to which it points.
This is taking advantage of the operator we already know, *, but it is also chaining the next pointer to be dereferenced.
This may seem confusing, but it helps when you look at other pointer implementations and you’ve seen this ** syntax.
Create a Go pointer using a different pointer syntax
The most common way to create Pointers is to use the syntax we discussed earlier. But there is another syntax, you can use the new() function to create Pointers.
Let’s look at an example snippet.
package main
import (
"fmt"
)
func main() {
pointer := new(int) // This will initialize the int to its zero value of 0
fmt.Println(pointer) // Aha! It's a pointer to: 0xc000018030
fmt.Println(*pointer) // Or, if we dereference: 0
}
Copy the code
The syntax is only slightly different, but all the principles we’ve already discussed are the same.
Common go pointer error concept
Reviewing everything we’ve learned, it’s useful to discuss some oft-repeated misconceptions when using Pointers.
When discussing Pointers, one oft-repeated statement is that Pointers are better at performance, which intuitively makes sense.
For example, if you pass a large structure to multiple different function calls, you can see that copying the structure multiple times into different functions can slow down the program’s performance.
But passing Pointers in Go tends to be slower than passing copied values.
This is because when a pointer is passed to a function, Go needs to do escape analysis to determine whether the value needs to be stored on the stack or heap.
Passing by value allows all variables to be stored on the stack, which means garbage collection of that variable can be skipped.
See the example program here.
func main() {
a := make([]*int, 1e9)
for i := 0; i < 10; i++ {
start := time.Now()
runtime.GC()
fmt.Printf("GC took %s\n", time.Since(start))
}
runtime.KeepAlive(a)
}
Copy the code
When allocating a billion Pointers, the garbage collector may take more than half a second. That’s less than a nanosecond per pointer. However, it can increase, especially when Pointers are used so heavily in a large code base with strong memory requirements.
If you use the same code as above instead of Pointers, the garbage collector can run a thousand times faster.
Test the performance of your use cases, because there are no hard and fast rules. Just remember the mantra “the pointer is always faster.” That’s not true in all cases.
conclusion
I hope that’s a useful summary. We’ve covered what Go Pointers are, the different ways to create Pointers, what problems they solve, and some of the things to be aware of when using them.
When I first learned about Pointers, I read a lot of large, well-written code libraries on GitHub (like Docker) to try to understand when and when not to use Pointers, and I encourage you to do the same.
This was very helpful in consolidating my knowledge and understanding in a hands-on way the different approaches the team was taking to reach the full potential of Pointers.
There are many problems to consider, for example.
- What did our performance tests show?
- What are the overall conventions in the broader code base?
- Does this make sense for this particular use case?
- Is it easy to read and understand what’s going on here?
Decisions about when and how to use Pointers are made on a case-by-case basis, and I hope you now have a full understanding of when best to use Pointers in your projects.
The postHow to use pointers in Goappeared first onLogRocket Blog.