• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

Justin Joyce

Practical tips and tutorials about software development.

  • Standing Invitation
  • Featured Posts
  • Latest
  • About

Pointers in Go

Posted Mar 4, 2023 — Updated Jan 10, 2024

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 myCarPointer1—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 type Car, and is the same type as Car{}. 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

  1. Adding “pointer” to the end of the variable name is not a standard practice, it’s just there for extra clarity in the example. ↩︎

Filed Under: Golang

Primary Sidebar

Recent Posts

  • Every Built-In Vim Color Scheme (with screenshots)
  • Reverse a string in Python
  • Meeting Cost Calculator
  • Vim find and replace
  • What makes an effective development team

Categories

  • Arrays (5)
  • Command Line (9)
  • Dates (3)
  • Featured (7)
  • Git (7)
  • Golang (5)
  • Javascript (8)
  • Productivity (8)
  • Projects (4)
  • Python (15)
  • Regex (2)
  • Ruby (3)
  • Shell (2)
  • Thoughts (2)
  • Tips (11)
  • Tools (3)
  • Tutorials (1)
  • Vim (4)

Archives

  • July 2024 (1)
  • February 2024 (1)
  • January 2024 (1)
  • December 2023 (1)
  • November 2023 (1)
  • October 2023 (4)
  • September 2023 (1)
  • August 2023 (2)
  • July 2023 (5)
  • June 2023 (3)
  • May 2023 (6)
  • April 2023 (5)
  • March 2023 (5)
  • February 2023 (10)
  • January 2023 (6)
  • December 2022 (7)

Copyright © 2025 · Contact me at justin [at] {this domain}

  • Privacy Policy