On this page

Pointers

14 min read TextCh. 3 — Data and Errors

Pointers in Go: Safety Without Complexity

A pointer is a variable that stores the memory address of another value. If x contains the value 42 at memory location 0xc00001e070, then a pointer to x contains the value 0xc00001e070.

Pointers are essential in many low-level languages, but notoriously difficult to handle correctly in C or C++. Go takes a middle ground: it includes pointers to give control and efficiency, but makes them safe by eliminating pointer arithmetic.

The `&` and `*` Operators

Go uses two fundamental operators for working with pointers:

  • & (address-of): returns the memory address of a variable
  • * (dereference): accesses the value the pointer points to
x := 42         // regular int variable
p := &x         // p is of type *int — "pointer to int"
                 // p holds x's memory address

fmt.Println(x)   // 42
fmt.Println(p)   // 0xc00001e070 (some address)
fmt.Println(*p)  // 42 — the value p points to

*p = 99          // modify the value through the pointer
fmt.Println(x)   // 99 — x was modified

Pointer Types

The type of a pointer is *T where T is the type it points to:

var pi *int         // pointer to int (nil by default)
var ps *string      // pointer to string (nil by default)
var pf *float64     // pointer to float64 (nil by default)

n := 42
pi = &n
fmt.Println(*pi)  // 42

Pass by Value vs. Pass by Reference

This is the most important distinction in pointer usage:

// PASS BY VALUE — function receives a complete copy
func doubleValue(n int) int {
    n *= 2
    return n  // returns the modified copy
}

// PASS BY REFERENCE (using pointer)
func doubleInPlace(n *int) {
    *n *= 2  // modifies the original value
}

func main() {
    a := 5

    result := doubleValue(a)
    fmt.Println(a, result)  // 5, 10 — a unchanged

    doubleInPlace(&a)
    fmt.Println(a)  // 10 — a modified
}

Structs: Value vs. Pointer

type Point struct{ X, Y float64 }

// Pass by value: function receives a complete copy of the struct
func moveCopy(p Point, dx, dy float64) Point {
    p.X += dx
    p.Y += dy
    return p
}

// Pass by pointer: more efficient and modifies the original
func moveOriginal(p *Point, dx, dy float64) {
    p.X += dx
    p.Y += dy
}

func main() {
    p := Point{X: 0, Y: 0}

    p2 := moveCopy(p, 5, 3)
    fmt.Println(p)  // {0 0} — unchanged
    fmt.Println(p2) // {5 3}

    moveOriginal(&p, 5, 3)
    fmt.Println(p)  // {5 3} — modified
}

When to Use Pointers

The general rule in Go:

Use a pointer when:

  1. The method needs to modify the receiver
  2. The struct is large (avoid costly copies — more than ~64 bytes)
  3. You need to represent an optional value (nil as "no value")
  4. Consistency: if any method of a type needs a pointer, make all methods use pointers

Use a value when:

  1. The type is small (int, string, struct with 1-2 fields)
  2. The type should not be modified (immutable types)
  3. Basic types: always by value (int, string, bool)
// Real examples of when to use a pointer

// 1. Method that modifies
func (c *Counter) Increment() { c.value++ }

// 2. Large struct by pointer
type Image struct {
    Pixels [1920][1080][3]uint8  // ~6MB — never copy
}
func processImage(img *Image) { /* ... */ }

// 3. Optional value
func findUser(id int) *User {
    // returns nil if not found
    if id == 0 {
        return nil
    }
    return &User{ID: id, Name: "Ana"}
}
user := findUser(1)
if user != nil {
    fmt.Println(user.Name)
}

The `new()` Function

new(T) allocates memory for a value of type T, initializes it to the zero value, and returns a pointer *T:

p := new(int)       // *int pointing to 0
*p = 42
fmt.Println(*p)     // 42

s := new(string)    // *string pointing to ""
*s = "hello"

cfg := new(Config)  // *Config with all fields at zero value
cfg.Host = "localhost"

// More idiomatic equivalent using a literal with &
cfg2 := &Config{Host: "localhost", Port: 8080}

In practice, new is used rarely. The more idiomatic form is &T{} or &T{field: value}.

Automatic Dereference in Structs

Go automatically dereferences pointers when accessing struct fields:

type Config struct {
    Host string
    Port int
}

cfg := &Config{Host: "localhost", Port: 8080}

// These two are equivalent
fmt.Println((*cfg).Host)  // explicit dereference (tedious)
fmt.Println(cfg.Host)      // Go does it automatically (idiomatic)

// Also works when calling methods
cfg.Enable()  // equivalent to (*cfg).Enable()

Nil Pointers: The Most Common Error

The most frequent pointer error in Go is dereferencing a nil pointer:

var p *int
fmt.Println(p)   // <nil>
fmt.Println(*p)  // PANIC: runtime error: invalid memory address or nil pointer dereference!

// Always check before dereferencing
if p != nil {
    fmt.Println(*p)
}

Safe Pattern with Optional Pointers

type User struct {
    Name     string
    LastName string
    Email    *string  // nil = not provided
}

u := User{
    Name:     "Ana",
    LastName: "Garcia",
}

// Check before accessing
if u.Email != nil {
    fmt.Println("Email:", *u.Email)
} else {
    fmt.Println("Email not provided")
}

// Assign a pointer to string
email := "[email protected]"
u.Email = &email

Pointers and Goroutines: Beware of Data Races

When multiple goroutines access the same data through pointers without synchronization, data races can occur:

// INCORRECT: data race
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++  // data race: multiple goroutines read and write
    }()
}
wg.Wait()

// CORRECT: use sync.Mutex or sync/atomic
import "sync/atomic"

var counter int64
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        atomic.AddInt64(&counter, 1)  // atomic operation
    }()
}
wg.Wait()

No Pointer Arithmetic

Go deliberately eliminates the pointer arithmetic found in C:

// In C: p++ advances the pointer to the next element
// In Go: this does not exist — use slices instead

nums := []int{10, 20, 30, 40, 50}

// Instead of pointer arithmetic, use slice indexing
for i := range nums {
    fmt.Println(nums[i])
}

// Or range
for _, n := range nums {
    fmt.Println(n)
}

This restriction makes Go code free from classic vulnerabilities like buffer overflows and use-after-free that are common in C/C++.

With pointers mastered, in the next lesson we will learn Go's error handling — the explicit, robust, and idiomatic way to manage failures without exceptions.

Dereferencing a nil pointer causes a panic
If you attempt to access the value of a nil pointer (with *p or p.Field), Go will panic at runtime. Always check if p != nil before dereferencing pointers that might be nil, especially those coming from functions or parameters.
Go has no pointer arithmetic
Unlike C or C++, you cannot do p++ or p+1 to advance a pointer to the next memory position. This restriction eliminates an entire category of security vulnerabilities (buffer overflows, use-after-free). If you need to index memory, use slices.