If you’re used to working in dynamic languages like Javascript, Ruby, or Python, Golang feels very different. One of the big differences is the explicit use of pointers. You might find yourself asking these questions:
- What the heck are
&
and*
doing? - When should I use them?
I have practical answers!
Golang ampersand &
and asterisk *
Obtain an address with &
Let’s start with &
. It has one use: obtaining a pointer address. Let’s say we have this simple definition for a Car
type:
type Car struct {
Brand string
Color string
}
Using the Car
struct type from above, we can create a new car like this:
// Shoutout to my 2007 Camry, it's still going strong
myCar := Car{Brand: "Toyota", Color: "Black"}
So far we’ve just created a new Car
object, we’re not using any pointers. However, in Golang you’ll often see &
used alongside object creation like this:
myCarPointer := &Car{Brand: "Toyota", Color: "Black"}
In the line above, we initialized a new car and used the &
to store its memory address in myCarPointer
1—pointers are just addresses in memory. If we printed the value of myCarPointer
it would look something like 0xc000014060
. To use the actual car object stored at that address, we need to dereference the pointer. That’s where *
comes in.
Dereferencing and declaring with *
Using myCarPointer
from above, we prefix it with *
to get to the object stored at that pointer’s address; this is known as pointer dereferencing. So *myCarPointer
will give us the actual object stored at the location of the pointer.
Confusingly, *
is also used to declare pointer types. If I want to declare something as a pointer to a Car
, I would write that as *Car
. In other words: the type of &Car{}
is *Car
.
Going further:
myCarPointer := &Car{}
myCarPointer
has type*Car
– it is a pointer to a Car*myCarPointer
has the typeCar
, and is the same type asCar{}
. It is a dereferenced pointer, which means it now is the object stored at the pointer’s location.
After all that you might be thinking, WHY would I use all this
?
When to use pointers
The general rule is: not unless you know you need to.
If you’re considering using pointers for performance reasons, you should benchmark your code—they’re not necessarily faster than passing values directly. Pointers can also open you up to the infamous nil pointer dereference bug, so it’s not purely a performance concern.
However, there are a few cases where you need pointers.
Mutating Objects
Golang is a pass-by-value language. In other words, Golang makes copies of objects passed to functions. This is the opposite of JS, Ruby, and Python, which all pass objects by reference. That means if you have some code that mutates an input in Go, you have to pass a pointer or it won’t work as you expect. We can see this with a quick example; let’s paint my car.
// No pointers in this example
func paintCar(c Car, color string) {
c.Color = color
// Print the color in here
log.Printf("Inside function, the car is %s", c.Color)
}
func main() {
myCar := Car{Brand: "Toyota", Color: "Black"}
log.Println(myCar.Color)
// Black
paintCar(myCar, "Blue")
// Inside function, the car is Blue
log.Println(myCar.Color)
// Black
}
In the example above, myCar.Color
didn’t change. That’s because we passed it in by value, and Go created a copy of myCar
inside the paintCar
function for use internally.
If we want to mutate the original car, we need a pointer:
// This time with pointers
func paintCarV2(c *Car, color string) {
c.Color = color
log.Printf("Inside function, the car is %s", c.Color)
}
func main() {
myCar := Car{Brand: "Toyota", Color: "Black"}
log.Println(myCar.Color)
// Black
// Use & to pass a pointer to myCar
paintCarV2(&myCar, "Blue")
// Inside function, the car is Blue
log.Println(myCar.Color)
// Blue
}
This time the car’s color was actually modified because we passed a pointer into the function. The function didn’t create a copy of an object, it used the pointer to find and mutate the original object itself.
Allowing for nil
values
In Go, if something is missing a value the compiler will assign it whatever the default value is for its type. Let’s flesh out the Car
type a bit more to illustrate:
type CarDetails struct {
WeightPounds int
MilesPerGallon int
}
type Car struct {
Brand string
Color string
Details CarDetails
}
// If we don't pass in any values, they'll be defaulted for us
myCar := Car{}
log.Printf("%+v", myCar)
// {Brand:"" Color:"" Details:{WeightPounds:0 MilesPerGallon:0}}
In this example we have useless car Details
. Why include them if we don’t know them? Obviously my car weighs more than 0
pounds, so that data is wrong. To avoid this, we need a pointer:
type Car struct {
Brand string
Color string
Details *CarDetails
}
myCar := Car{}
log.Printf("%+v", myCar)
// {Brand:"" Color:"" Details:<nil>}
Now the details are <nil>
, or more accurately a nil pointer
, and if we serialize them with something like omitempty
the details won’t be included.
There are some other times you might want to use pointers, like pointer receivers, but those are more complex and this post is long enough already.
Conclusion
- What is
&
used for? – Obtaining pointer addresses - What is
*
used for – Declaring pointer types and dereferencing existing pointers - When should you use pointers? Only if you know for sure you need to, otherwise just use regular values.
Helpful Links
- A great Stackoverflow response about exactly this topic
- Address operators
- Stackoverflow thread about omitempty
- encoding/json docs
Notes
- Adding “pointer” to the end of the variable name is not a standard practice, it’s just there for extra clarity in the example. ↩︎