Pointers And Memory Management In Go: How Pointers Work In Go And Their Role In Efficient Memory Usage
Overview
This chapter introduces pointers in Go, demystifying their role in memory management and efficient data handling. You’ll learn the difference between passing by value and passing by reference, how to declare and use pointers, and when to leverage pointers for better performance and flexibility. The chapter will also explore the security implications of pointers, the basics of pointer dereferencing, and how Go’s memory management techniques avoid common pitfalls seen in other languages. Through practical examples and exercises, you will gain a deeper understanding of when and how to use pointers effectively in Go.
Pointers And Memory Management In Go: How Pointers Work In Go And Their Role In Efficient Memory Usage
People are afraid of pointers. In other languages you hear horror stories about pointer arithmetic, overriding memory by accident, and other terrible tales.
At their core, pointers in Go, are fairly simple and straightforward.
Values
By default, Go handles all variables as values. This means that any time we work with a variable, the scope plays a part as to whether we are modifying the original value, or a copy of the value.
For instance, within the scope of a function, you are always working with the local copy (or original value) of a variable, and as such, you will always make changes to the original value. This is common to most programming languages and you would expect it to work this way.
The below example shows that if we have a struct called Beatle and change the Name field locally, that we do in fact change the actual original value. This is because the variable is local to our scope and we have access to the original value.
package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
fmt.Println("Name is:", b.Name)
// Change the name to George
b.Name = "George"
fmt.Println("Name is:", b.Name)
}package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
fmt.Println("Name is:", b.Name)
// Change the name to George
b.Name = "George"
fmt.Println("Name is:", b.Name)
}Output:
Name is: Ringo
Name is: GeorgeName is: Ringo
Name is: GeorgeAgain, this is what most developers would expect from most programming languages.
Passing The Value
To understand pointers we must first understand how Go passes data. It does this by passing around a copy of the data, this is called “pass by value”.
package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
changeBeatleName(b)
fmt.Println(b.Name) // Ringo
}
func changeBeatleName(b Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}
package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
changeBeatleName(b)
fmt.Println(b.Name) // Ringo
}
// section: change
func changeBeatleName(b Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}
// section: changeThe changeBeatleName function is receiving a copy of the Beatle value.
package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
changeBeatleName(b)
fmt.Println(b.Name) // Ringo
}
// section: change
func changeBeatleName(b Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}
// section: changeIt can not modify the original value.
Go passes all data by value, including pointers.
Pointer Syntax
Pointers allow us to point at the original memory space of a value.
To indicate we are expecting a pointer, we use the * modifier:
func changeBeatleName(b *Beatle) {}
To get the address of a value we use the & modifier:
&Beatle{}
And we can store pointers too:
b := &Beatle{}
Passing By Pointer
In the following code, we changed the function argument to take a pointer to Beatle now, instead of an actual Beatle type.
The change is subtle, but important. As you can see, the argument is now *Beatle and not Beatle. This tells the function to expect a pointer instead of a value.
This also now allows us to mutate/change the original variable that was passed in, as we now have a reference to it via the pointer that was passed into the function.
package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := &Beatle{Name: "Ringo"}
changeBeatleName(b)
fmt.Println(b.Name) // George
}
func changeBeatleName(b *Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := &Beatle{Name: "Ringo"}
changeBeatleName(b)
fmt.Println(b.Name) // George
}
func changeBeatleName(b *Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}You can also create the original Beatle as a value (not a pointer) and pass it as a pointer to the function. When you do one over the other is sometimes a matter of style, and sometimes depends on your intent of the code when you wrote it. Neither is right or wrong, nor is one more performant than the other.
package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
changeBeatleName(&b)
fmt.Println(b.Name) // George
}
func changeBeatleName(b *Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}package main
import "fmt"
type Beatle struct {
Name string
}
func main() {
b := Beatle{Name: "Ringo"}
changeBeatleName(&b)
fmt.Println(b.Name) // George
}
func changeBeatleName(b *Beatle) {
b.Name = "George"
fmt.Println(b.Name) // George
}Dereferencing
We can dereference a pointer with the * as well.
// Create a new pointer to a string
ps := new(string)
// Set the value of ps by dereferencing the pointer
*ps = "hello"
// Capture a new "value" as a string
s := *ps
package main
import (
"fmt"
)
func main() {
// section: main
// Create a new pointer to a string
ps := new(string)
// Set the value of ps by dereferencing the pointer
*ps = "hello"
// Capture a new "value" as a string
s := *ps
// section: main
fmt.Printf("ps type:\t\t%T\nps value:\t\t%v\ndereferenced value:\t%q\n", ps, ps, *ps)
fmt.Printf("s type:\t\t\t%T\ns value:\t\t%v\n", s, s)
}
/*
// section: output
ps type: *string
ps value: 0xc000092030
dereferenced value: "hello"
s type: string
s value: hello
// section: output
*/Output:
package main
import (
"fmt"
)
func main() {
// section: main
// Create a new pointer to a string
ps := new(string)
// Set the value of ps by dereferencing the pointer
*ps = "hello"
// Capture a new "value" as a string
s := *ps
// section: main
fmt.Printf("ps type:\t\t%T\nps value:\t\t%v\ndereferenced value:\t%q\n", ps, ps, *ps)
fmt.Printf("s type:\t\t\t%T\ns value:\t\t%v\n", s, s)
}
/*
// section: output
ps type: *string
ps value: 0xc000092030
dereferenced value: "hello"
s type: string
s value: hello
// section: output
*/New
The built-in function new takes a type T, allocates storage for a variable of that type at run time, and returns a value of type *T pointing to it. The variable is initialized with it’s zero value.
s := new(string)
i := new(int)
type foo struct {
id int
name string
}
f := new(foo)
// Functionally equivalent to this:
f1 := &foo{} // this is the idiomatic preferred style now
fmt.Printf("s: %v, *s: %q\n", s, *s)
fmt.Printf("i: %v, *i: %d\n", i, *i)
fmt.Printf("f: %v, *f: %+v\n", f, *f)
fmt.Printf("f1: %v, *f1: %+v\n", f1, *f1)
package main
import "fmt"
func main() {
// section: main
s := new(string)
i := new(int)
type foo struct {
id int
name string
}
f := new(foo)
// Functionally equivalent to this:
f1 := &foo{} // this is the idiomatic preferred style now
fmt.Printf("s: %v, *s: %q\n", s, *s)
fmt.Printf("i: %v, *i: %d\n", i, *i)
fmt.Printf("f: %v, *f: %+v\n", f, *f)
fmt.Printf("f1: %v, *f1: %+v\n", f1, *f1)
// section: main
}
/*
// section: output
s: 0xc000092030, *s: ""
i: 0xc000094000, *i: 0
f: &{0 }, *f: {id:0 name:}
f1: &{0 }, *f1: {id:0 name:}
// section: output
*/Output:
s: 0xc000092030, *s: ""
i: 0xc000094000, *i: 0
f: &{0 }, *f: {id:0 name:}
f1: &{0 }, *f1: {id:0 name:}package main
import "fmt"
func main() {
// section: main
s := new(string)
i := new(int)
type foo struct {
id int
name string
}
f := new(foo)
// Functionally equivalent to this:
f1 := &foo{} // this is the idiomatic preferred style now
fmt.Printf("s: %v, *s: %q\n", s, *s)
fmt.Printf("i: %v, *i: %d\n", i, *i)
fmt.Printf("f: %v, *f: %+v\n", f, *f)
fmt.Printf("f1: %v, *f1: %+v\n", f1, *f1)
// section: main
}
/*
// section: output
s: 0xc000092030, *s: ""
i: 0xc000094000, *i: 0
f: &{0 }, *f: {id:0 name:}
f1: &{0 }, *f1: {id:0 name:}
// section: output
*/New With Expressions (Go 1.26)
Go will not let you take the address of a literal directly:
timeout := &30 // does not compile
This comes up a lot with optional struct fields, where a pointer field uses nil to mean “not set”:
package main
import "fmt"
// section: helper
// ptr returns a pointer to any value passed to it. Before Go 1.26,
// helpers like this were a common workaround for taking the address
// of a literal.
func ptr[T any](v T) *T {
return &v
}
// section: helper
// section: server
type Server struct {
Name string
Timeout *int // nil means use the default timeout
}
// section: server
func main() {
// section: main
// This will not compile. You can't take the address of a literal:
// timeout := &30
// Before Go 1.26, you assigned to a variable first, or used a helper:
seconds := 30
api := Server{Name: "api", Timeout: &seconds}
web := Server{Name: "web", Timeout: ptr(60)}
// As of Go 1.26, new accepts an expression:
cache := Server{Name: "cache", Timeout: new(90)}
fmt.Printf("%s timeout: %d\n", api.Name, *api.Timeout)
fmt.Printf("%s timeout: %d\n", web.Name, *web.Timeout)
fmt.Printf("%s timeout: %d\n", cache.Name, *cache.Timeout)
// section: main
}
/*
// section: output
api timeout: 30
web timeout: 60
cache timeout: 90
// section: output
*/Before Go 1.26, you had two options. You could assign the value to a variable first and take the address of that, or you could write a small generic helper. You’ll still find helpers like this one in a lot of codebases:
// ptr returns a pointer to any value passed to it. Before Go 1.26,
// helpers like this were a common workaround for taking the address
// of a literal.
func ptr[T any](v T) *T {
return &v
}
package main
import "fmt"
// section: helper
// ptr returns a pointer to any value passed to it. Before Go 1.26,
// helpers like this were a common workaround for taking the address
// of a literal.
func ptr[T any](v T) *T {
return &v
}
// section: helper
// section: server
type Server struct {
Name string
Timeout *int // nil means use the default timeout
}
// section: server
func main() {
// section: main
// This will not compile. You can't take the address of a literal:
// timeout := &30
// Before Go 1.26, you assigned to a variable first, or used a helper:
seconds := 30
api := Server{Name: "api", Timeout: &seconds}
web := Server{Name: "web", Timeout: ptr(60)}
// As of Go 1.26, new accepts an expression:
cache := Server{Name: "cache", Timeout: new(90)}
fmt.Printf("%s timeout: %d\n", api.Name, *api.Timeout)
fmt.Printf("%s timeout: %d\n", web.Name, *web.Timeout)
fmt.Printf("%s timeout: %d\n", cache.Name, *cache.Timeout)
// section: main
}
/*
// section: output
api timeout: 30
web timeout: 60
cache timeout: 90
// section: output
*/As of Go 1.26, new also accepts an expression. It allocates storage, copies the value in, and returns the pointer, all in one step:
// This will not compile. You can't take the address of a literal:
// timeout := &30
// Before Go 1.26, you assigned to a variable first, or used a helper:
seconds := 30
api := Server{Name: "api", Timeout: &seconds}
web := Server{Name: "web", Timeout: ptr(60)}
// As of Go 1.26, new accepts an expression:
cache := Server{Name: "cache", Timeout: new(90)}
fmt.Printf("%s timeout: %d\n", api.Name, *api.Timeout)
fmt.Printf("%s timeout: %d\n", web.Name, *web.Timeout)
fmt.Printf("%s timeout: %d\n", cache.Name, *cache.Timeout)
package main
import "fmt"
// section: helper
// ptr returns a pointer to any value passed to it. Before Go 1.26,
// helpers like this were a common workaround for taking the address
// of a literal.
func ptr[T any](v T) *T {
return &v
}
// section: helper
// section: server
type Server struct {
Name string
Timeout *int // nil means use the default timeout
}
// section: server
func main() {
// section: main
// This will not compile. You can't take the address of a literal:
// timeout := &30
// Before Go 1.26, you assigned to a variable first, or used a helper:
seconds := 30
api := Server{Name: "api", Timeout: &seconds}
web := Server{Name: "web", Timeout: ptr(60)}
// As of Go 1.26, new accepts an expression:
cache := Server{Name: "cache", Timeout: new(90)}
fmt.Printf("%s timeout: %d\n", api.Name, *api.Timeout)
fmt.Printf("%s timeout: %d\n", web.Name, *web.Timeout)
fmt.Printf("%s timeout: %d\n", cache.Name, *cache.Timeout)
// section: main
}
/*
// section: output
api timeout: 30
web timeout: 60
cache timeout: 90
// section: output
*/Output:
package main
import "fmt"
// section: helper
// ptr returns a pointer to any value passed to it. Before Go 1.26,
// helpers like this were a common workaround for taking the address
// of a literal.
func ptr[T any](v T) *T {
return &v
}
// section: helper
// section: server
type Server struct {
Name string
Timeout *int // nil means use the default timeout
}
// section: server
func main() {
// section: main
// This will not compile. You can't take the address of a literal:
// timeout := &30
// Before Go 1.26, you assigned to a variable first, or used a helper:
seconds := 30
api := Server{Name: "api", Timeout: &seconds}
web := Server{Name: "web", Timeout: ptr(60)}
// As of Go 1.26, new accepts an expression:
cache := Server{Name: "cache", Timeout: new(90)}
fmt.Printf("%s timeout: %d\n", api.Name, *api.Timeout)
fmt.Printf("%s timeout: %d\n", web.Name, *web.Timeout)
fmt.Printf("%s timeout: %d\n", cache.Name, *cache.Timeout)
// section: main
}
/*
// section: output
api timeout: 30
web timeout: 60
cache timeout: 90
// section: output
*/As you can see, all three forms produce a valid pointer. The new(90) form replaces both the extra variable and the helper function, so when you run into a ptr() style helper in an older codebase, this is what it was working around.
Pointers In Structs
Care must be taken when passing structs by value that have pointers as fields. Even though everything is passed as a value (i.e. copy of the original), if it is a pointer, it still points to the original value, thus allowing for it to be modified.
As you can see in the below code, even though we pass by value, and can’t modify the First and Last fields, we are able to modify the email field because it’s a pointer in the struct.
package main
import (
"fmt"
)
type User struct {
First string
Last string
Email *string
}
func main() {
u := User{}
u.First = "Rob"
u.Last = "Pike"
u.Email = new(string)
*(u.Email) = "commander@google.com"
fmt.Println(u.First, u.Last, *u.Email)
reset(u)
fmt.Println(u.First, u.Last, *u.Email)
}
// even though we pass by value, it has an underlying pointer, which we have reference to a memory space and can mutate that variable.
func reset(u User) {
u.First = ""
u.Last = ""
*(u.Email) = "nobody@google.com"
}package main
import (
"fmt"
)
type User struct {
First string
Last string
Email *string
}
func main() {
u := User{}
u.First = "Rob"
u.Last = "Pike"
u.Email = new(string)
*(u.Email) = "commander@google.com"
fmt.Println(u.First, u.Last, *u.Email)
reset(u)
fmt.Println(u.First, u.Last, *u.Email)
}
// even though we pass by value, it has an underlying pointer, which we have reference to a memory space and can mutate that variable.
func reset(u User) {
u.First = ""
u.Last = ""
*(u.Email) = "nobody@google.com"
}Output:
Rob Pike commander@google.com
Rob Pike nobody@google.comRob Pike commander@google.com
Rob Pike nobody@google.comEven though the User is passed by value, the Email is a pointer to a string. A copy of a pointer is still the original address of the value being pointed at. As such, you can expose fields unintentionally when they are a pointer in a struct.
Panic
Accessing a pointer that has not been initialized will result in a runtime panic:
package main
func main() {
// section: main
var s *string
*s = "boom"
// section: main
}
/*
// section: output
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x104ea8d]
goroutine 1 [running]:
main.main()
/Users/admin/go/src/github.com/gopherguides/learn/_training/fundamentals/pointers/src/panic.go:6 +0x1d
exit status 2
*/Output:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x104ea8d]
goroutine 1 [running]:
main.main()
/Users/admin/go/src/github.com/gopherguides/learn/_training/fundamentals/pointers/src/panic.go:6 +0x1d
exit status 2
*/package main
func main() {
// section: main
var s *string
*s = "boom"
// section: main
}
/*
// section: output
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x104ea8d]
goroutine 1 [running]:
main.main()
/Users/admin/go/src/github.com/gopherguides/learn/_training/fundamentals/pointers/src/panic.go:6 +0x1d
exit status 2
*/Security
One of the benefits of Go’s default “pass by value” strategy is security. By defaulting to creating a copy of a value when it is passed we can rest assured knowing that our values can not be mutated by other functions and actors.
Should we want to allow others to modify our values we can pass them a pointer to the value and they will be able to modify the value accordingly.
Performance
Pointers in Go can both help, and hurt, performance. In the case of a large chunk of memory (think an image file) passing a pointer around can reduce memory allocation and operational overhead.
However, pointers also indicate to the garbage collector and the compiler that the value in question might be used outside of its current location, which may place the value on the heap vs. the stack.
When To Use Pointers?
Use pointers if:
- You want others to be able to modify your values or
- You have a large (memory) value that you don’t want to keep copying (This is typically done as a result of profiling and you have proven this is an actual performance issue)
Exercise (10 Mins)
package main
import "fmt"
func main() {
x := "George"
printValue(&x)
fmt.Println(x)
// Classic form (works in any Go version):
// using short variable declaration and the `new` keyword,
// create a variable named `s` that is a pointer to a string
// set the value of `s` to "hello"
// print out both the pointer value and the dereferenced value of `s`
// Go 1.26 form:
// using `new` with an expression, create a variable named `s2` that is
// a pointer to a string initialized to "hello" in a single step
// print out both the pointer value and the dereferenced value of `s2`
}
func printValue(s *string) {
// print the pointer value
// print the string value
// change the string value
}package main
import "fmt"
func main() {
x := "George"
printValue(&x)
fmt.Println(x)
// Classic form (works in any Go version):
// using short variable declaration and the `new` keyword,
// create a variable named `s` that is a pointer to a string
// set the value of `s` to "hello"
// print out both the pointer value and the dereferenced value of `s`
// Go 1.26 form:
// using `new` with an expression, create a variable named `s2` that is
// a pointer to a string initialized to "hello" in a single step
// print out both the pointer value and the dereferenced value of `s2`
}
func printValue(s *string) {
// print the pointer value
// print the string value
// change the string value
}Solution
package main
import "fmt"
func main() {
x := "George"
printValue(&x)
fmt.Println(x)
// Classic form: allocate a zero-valued string, then assign it.
s := new(string)
*s = "hello"
fmt.Println(s, *s)
// Go 1.26 form: new accepts an expression and does both in one step.
s2 := new("hello")
fmt.Println(s2, *s2)
}
func printValue(s *string) {
fmt.Println(s)
fmt.Println(*s)
*s = "Ringo"
}package main
import "fmt"
func main() {
x := "George"
printValue(&x)
fmt.Println(x)
// Classic form: allocate a zero-valued string, then assign it.
s := new(string)
*s = "hello"
fmt.Println(s, *s)
// Go 1.26 form: new accepts an expression and does both in one step.
s2 := new("hello")
fmt.Println(s2, *s2)
}
func printValue(s *string) {
fmt.Println(s)
fmt.Println(*s)
*s = "Ringo"
}