In the previous tutorial, you learned about interfaces and polymorphism. Now it is time to learn about pointers — how Go lets you share and modify data efficiently.
If you have used C or C++, you know pointers. Go pointers are simpler. There is no pointer arithmetic. You cannot do math on pointer addresses. This makes Go pointers safe and easy to understand.
What is a Pointer?
A pointer holds the memory address of a value. Instead of copying the value, you pass its address. This lets functions modify the original value:
package main
import "fmt"
func main() {
name := "Alex"
// & gives the address of a variable
ptr := &name
fmt.Println("Value:", name) // Alex
fmt.Println("Address:", ptr) // 0xc000014070 (some memory address)
fmt.Println("Type:", fmt.Sprintf("%T", ptr)) // *string
// * reads the value at the address (dereference)
fmt.Println("Dereferenced:", *ptr) // Alex
}
Output:
Value: Alex
Address: 0xc000014070
Type: *string
Dereferenced: Alex
Two operators to remember:
&— “address of” — gives you a pointer to a variable*— “dereference” — gives you the value a pointer points to
Modifying Values Through Pointers
The main reason to use pointers is to modify a value from a different function:
package main
import "fmt"
// Without pointer — gets a COPY, changes are lost
func doubleValue(n int) {
n = n * 2
fmt.Println("Inside doubleValue:", n) // 20
}
// With pointer — modifies the ORIGINAL
func doublePointer(n *int) {
*n = *n * 2
fmt.Println("Inside doublePointer:", *n) // 20
}
func main() {
num := 10
doubleValue(num)
fmt.Println("After doubleValue:", num) // 10 — unchanged
doublePointer(&num)
fmt.Println("After doublePointer:", num) // 20 — changed
}
Output:
Inside doubleValue: 20
After doubleValue: 10
Inside doublePointer: 20
After doublePointer: 20
Without a pointer, the function works on a copy. With a pointer, it modifies the original. This is important for functions that need to update the caller’s data.
Pointers with Structs
Pointers are commonly used with structs. Structs can be large, so passing a pointer avoids copying all the data:
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
// Accepts a pointer — can modify the user
func celebrateBirthday(u *User) {
u.Age++
fmt.Printf("Happy birthday, %s! You are now %d.\n", u.Name, u.Age)
}
// Accepts a value — gets a copy
func printUser(u User) {
fmt.Printf("User: %s (%s, age %d)\n", u.Name, u.Email, u.Age)
}
func main() {
user := User{Name: "Alex", Email: "alex@example.com", Age: 25}
printUser(user)
celebrateBirthday(&user)
printUser(user)
}
Output:
User: Alex (alex@example.com, age 25)
Happy birthday, Alex! You are now 26.
User: Alex (alex@example.com, age 26)
Notice: Go automatically dereferences struct pointers. You write u.Age, not (*u).Age. Both work, but the short form is standard.
Creating Structs with Pointers
You can create a pointer to a struct directly with &:
package main
import "fmt"
type Config struct {
Host string
Port int
}
func NewConfig(host string, port int) *Config {
return &Config{
Host: host,
Port: port,
}
}
func main() {
cfg := NewConfig("localhost", 8080)
fmt.Printf("Host: %s, Port: %d\n", cfg.Host, cfg.Port)
}
This is the common pattern for constructor functions in Go. Return a *Config (pointer), not a Config (value).
Pointer Receivers on Methods
You already saw pointer receivers in the structs tutorial. Here is a deeper look:
package main
import "fmt"
type Stack struct {
items []int
}
// Pointer receiver — modifies the stack
func (s *Stack) Push(item int) {
s.items = append(s.items, item)
}
// Pointer receiver — modifies the stack
func (s *Stack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
last := len(s.items) - 1
item := s.items[last]
s.items = s.items[:last]
return item, true
}
// Value receiver — only reads the stack
func (s Stack) Peek() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
return s.items[len(s.items)-1], true
}
// Value receiver — only reads
func (s Stack) Size() int {
return len(s.items)
}
func main() {
var s Stack
s.Push(10)
s.Push(20)
s.Push(30)
fmt.Println("Size:", s.Size())
if val, ok := s.Peek(); ok {
fmt.Println("Top:", val)
}
for s.Size() > 0 {
if val, ok := s.Pop(); ok {
fmt.Printf("Popped: %d\n", val)
}
}
fmt.Println("Size after popping all:", s.Size())
}
Output:
Size: 3
Top: 30
Popped: 30
Popped: 20
Popped: 10
Size after popping all: 0
Rule of thumb: If any method needs a pointer receiver, use pointer receivers for all methods on that type. This avoids confusion.
nil Pointers
A pointer that does not point to anything has the value nil:
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func main() {
var ptr *int
fmt.Println("Nil pointer:", ptr) // <nil>
// Check for nil before using
if ptr != nil {
fmt.Println("Value:", *ptr)
} else {
fmt.Println("Pointer is nil")
}
// Practical example: linked list
node3 := &Node{Value: 3, Next: nil}
node2 := &Node{Value: 2, Next: node3}
node1 := &Node{Value: 1, Next: node2}
// Walk the linked list
current := node1
for current != nil {
fmt.Printf("%d -> ", current.Value)
current = current.Next
}
fmt.Println("nil")
}
Output:
Nil pointer: <nil>
Pointer is nil
1 -> 2 -> 3 -> nil
Important: Dereferencing a nil pointer causes a panic (crash):
var ptr *int
fmt.Println(*ptr) // panic: runtime error: invalid memory address or nil pointer dereference
Always check for nil before dereferencing a pointer.
The new Function
The new function allocates memory and returns a pointer. The value is set to its zero value:
package main
import "fmt"
func main() {
// new returns a pointer to a zero-valued int
ptr := new(int)
fmt.Println("Value:", *ptr) // 0
fmt.Println("Type:", fmt.Sprintf("%T", ptr)) // *int
*ptr = 42
fmt.Println("Updated:", *ptr) // 42
// For structs, &Type{} is more common than new(Type)
type Point struct{ X, Y int }
p1 := new(Point) // Less common
p2 := &Point{X: 1, Y: 2} // More common
fmt.Println("p1:", *p1)
fmt.Println("p2:", *p2)
}
In practice, most Go developers use &Type{} instead of new(Type) for structs. The new function is more common for basic types.
When to Use Pointers vs Values
Here is a simple guide:
Use pointers when:
- The function needs to modify the caller’s data
- The struct is large (avoids copying)
- You need to represent “no value” with
nil - Consistency — if some methods use pointer receivers, all should
Use values when:
- The data is small (int, bool, small structs)
- You want immutability — the function should not modify the original
- The type is a map, slice, or channel (they are already reference types)
package main
import "fmt"
type SmallPoint struct {
X, Y int
}
type LargeData struct {
Name string
Records [1000]int
Buffer [4096]byte
}
// Value — SmallPoint is small, no need for a pointer
func distance(p SmallPoint) float64 {
return float64(p.X*p.X + p.Y*p.Y)
}
// Pointer — LargeData is big, avoid copying
func process(d *LargeData) {
d.Name = "processed"
fmt.Println("Processing:", d.Name)
}
func main() {
point := SmallPoint{X: 3, Y: 4}
fmt.Printf("Distance: %.1f\n", distance(point))
data := LargeData{Name: "raw"}
process(&data)
fmt.Println("After process:", data.Name)
}
Note about slices and maps: Slices and maps are already reference types internally. When you pass a slice to a function, the function can modify the slice’s elements. You only need a pointer if you want to change the slice itself (like appending to it):
package main
import "fmt"
// No pointer needed — can modify elements
func doubleAll(nums []int) {
for i := range nums {
nums[i] *= 2
}
}
// Pointer needed — modifies the slice itself
func addElement(nums *[]int, val int) {
*nums = append(*nums, val)
}
func main() {
numbers := []int{1, 2, 3}
doubleAll(numbers)
fmt.Println("Doubled:", numbers) // [2 4 6]
addElement(&numbers, 100)
fmt.Println("Added:", numbers) // [2 4 6 100]
}
No Pointer Arithmetic
In C, you can do math on pointers to walk through memory. Go does not allow this:
// This does NOT work in Go
ptr := &someArray[0]
ptr++ // Compile error: cannot do pointer arithmetic
This is a safety feature. Pointer arithmetic is a common source of bugs in C and C++. Go removes it entirely.
Go Pointers vs Rust Borrowing
If you are also learning Rust (from the Rust Tutorial series), here is how they compare:
| Feature | Go | Rust |
|---|---|---|
| Syntax | *T pointer, &x address | &T reference, &mut T mutable reference |
| Null safety | nil pointers (runtime panic) | No null (Option<T> instead) |
| Pointer arithmetic | Not allowed | Not allowed (in safe Rust) |
| Multiple mutable references | Allowed (programmer’s responsibility) | Not allowed (enforced at compile time) |
| Garbage collection | Yes | No (ownership system) |
Go is simpler. You get pointers and the garbage collector handles memory. Rust is stricter. The borrow checker prevents data races at compile time but has a steeper learning curve.
For more on Rust’s approach, see Rust Tutorial #5: Borrowing and References.
A Complete Example
Here is a program that uses pointers in a practical way:
package main
import "fmt"
type TreeNode struct {
Value int
Left *TreeNode
Right *TreeNode
}
// Insert adds a value to the binary search tree
func (n *TreeNode) Insert(val int) {
if val < n.Value {
if n.Left == nil {
n.Left = &TreeNode{Value: val}
} else {
n.Left.Insert(val)
}
} else {
if n.Right == nil {
n.Right = &TreeNode{Value: val}
} else {
n.Right.Insert(val)
}
}
}
// InOrder prints the tree in sorted order
func (n *TreeNode) InOrder() {
if n == nil {
return
}
n.Left.InOrder()
fmt.Printf("%d ", n.Value)
n.Right.InOrder()
}
// Search checks if a value exists
func (n *TreeNode) Search(val int) bool {
if n == nil {
return false
}
if val == n.Value {
return true
}
if val < n.Value {
return n.Left.Search(val)
}
return n.Right.Search(val)
}
func main() {
fmt.Println("=== GO-9: Pointers ===")
fmt.Println()
// Build a binary search tree
root := &TreeNode{Value: 50}
values := []int{30, 70, 20, 40, 60, 80}
for _, v := range values {
root.Insert(v)
}
// Print in sorted order
fmt.Print("In-order traversal: ")
root.InOrder()
fmt.Println()
// Search for values
fmt.Println()
for _, v := range []int{40, 55, 70} {
if root.Search(v) {
fmt.Printf("Found %d in the tree\n", v)
} else {
fmt.Printf("%d is NOT in the tree\n", v)
}
}
}
Output:
=== GO-9: Pointers ===
In-order traversal: 20 30 40 50 60 70 80
Found 40 in the tree
55 is NOT in the tree
Found 70 in the tree
This example shows pointers in their natural habitat: tree data structures where nodes point to other nodes.
Common Mistakes
1. Dereferencing a nil pointer.
var user *User
fmt.Println(user.Name) // panic!
// Always check for nil
if user != nil {
fmt.Println(user.Name)
}
This is the most common pointer mistake in Go. Always check for nil before using a pointer.
2. Returning a pointer to a local variable (this is actually fine in Go).
// This is SAFE in Go — the compiler moves the variable to the heap
func newUser() *User {
user := User{Name: "Alex"}
return &user // Safe! Go's escape analysis handles this
}
In C, returning a pointer to a local variable is dangerous. In Go, it is safe. The compiler detects this and allocates the variable on the heap instead of the stack.
3. Unnecessary pointers for small types.
// Unnecessary — int is small, just copy it
func process(n *int) { ... }
// Better — just pass the value
func process(n int) int { ... }
Do not use pointers for small types unless you need to modify the original. Passing a copy is fast and simpler.
Source Code
You can find the complete source code for this tutorial on GitHub:
Related Articles
- Go Tutorial #8: Interfaces and Polymorphism — Interfaces in Go
- Go Tutorial #7: Structs, Methods, and Composition — Pointer receivers on methods
- Rust Tutorial #5: Borrowing and References — Rust’s approach to references
What’s Next?
In the next tutorial, Go Tutorial #10: Project Structure and Clean Architecture, you will learn:
- The standard Go project layout (
cmd/,internal/,pkg/) - How to organize packages
- Dependency injection without a framework
- The repository and service layer patterns
- When to split code into packages vs keep it simple
This is part 9 of the Go Tutorial series. Follow along to learn Go from scratch.