Skip to content

Pointers Beginner

Introduction

A pointer holds the memory address of a value. Go uses pointers to enable mutation of shared data, avoid copying large structures, and express optionality (a pointer can be nil). Unlike C/C++, Go has no pointer arithmetic — pointers are safer and simpler while still giving you direct memory control.

Syntax & Usage

Declaration and Basic Operations

var x int = 42

var p *int = &x  // p holds the address of x
fmt.Println(p)   // 0xc0000b6010 (some memory address)
fmt.Println(*p)  // 42 -- dereference: read the value at that address

*p = 100         // modify x through the pointer
fmt.Println(x)   // 100
Operator Name Purpose
&x Address-of Get the memory address of x
*p Dereference Read/write the value at address
*T Pointer type Declares a pointer to type T

Zero Value of a Pointer Is nil

var p *int       // nil -- points to nothing
fmt.Println(p)   // <nil>
// fmt.Println(*p) // PANIC: runtime error: invalid memory address

if p != nil {
    fmt.Println(*p)
}

The new() Function

new(T) allocates zeroed memory for type T and returns a *T.

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

new() vs composite literals

In practice, composite literals with & are far more common than new():

// Preferred -- more readable, allows initialization
cfg := &Config{Timeout: 30, Retries: 3}

// Rarely used
cfg := new(Config)
cfg.Timeout = 30
cfg.Retries = 3

Pointers as Function Parameters (Mutation)

func increment(val int) {
    val++ // modifies a copy -- caller's value unchanged
}

func incrementPtr(val *int) {
    *val++ // modifies the original
}

x := 10
increment(x)
fmt.Println(x)    // 10 -- unchanged

incrementPtr(&x)
fmt.Println(x)    // 11 -- modified

Pointers to Structs

Go automatically dereferences struct pointers — you don't need (*p).Field.

type User struct {
    Name  string
    Email string
}

u := &User{Name: "Alice", Email: "alice@example.com"}
fmt.Println(u.Name)  // "Alice" -- no (*u).Name needed
u.Email = "new@example.com"

Pointer to Pointer

Rare in Go, but know it exists.

x := 42
p := &x
pp := &p   // **int

fmt.Println(**pp) // 42

Pointer Receivers vs Value Receivers

type Counter struct {
    n int
}

// Value receiver -- operates on a copy
func (c Counter) Value() int {
    return c.n
}

// Pointer receiver -- can mutate the original
func (c *Counter) Increment() {
    c.n++
}

c := Counter{}
c.Increment() // Go auto-takes address: (&c).Increment()
fmt.Println(c.Value()) // 1

Quick Reference

Concept Syntax Notes
Declare pointer var p *int Zero value is nil
Address-of p = &x Get address of x
Dereference *p Read/write value at address
Allocate new(T) Returns *T, zeroed
Composite literal &Config{...} Preferred over new()
Nil check if p != nil Always check before dereferencing
Pointer receiver func (s *S) M() Can mutate, can be nil
Value receiver func (s S) M() Operates on copy
Pointer arithmetic Not supported Go is not C

Best Practices

  1. Use pointers for mutation: If a function needs to modify the caller's data, pass a pointer.
  2. Use pointers for large structs: Avoids copying overhead for structs with many fields or large slices.
  3. Use pointers for optional/nilability: A *string can be nil, a string cannot — useful for distinguishing "not set" from empty.
  4. Prefer value receivers when the method doesn't mutate: Makes the intent clear and is safe for concurrent use.
  5. Be consistent within a type: If any method needs a pointer receiver, use pointer receivers for all methods on that type.
  6. Don't return pointers to loop variables unless you understand capture semantics.

Common Pitfalls

Nil pointer dereference

The most common runtime panic in Go. Always check for nil before dereferencing.

func findUser(id int) *User { return nil }

u := findUser(42)
fmt.Println(u.Name) // PANIC -- u is nil

Pointer to loop variable

Prior to Go 1.22, the loop variable was reused across iterations.

// Go < 1.22: all pointers point to the same variable
var ptrs []*int
for i := 0; i < 3; i++ {
    ptrs = append(ptrs, &i) // BUG in Go < 1.22
}
// Go 1.22+: each iteration gets a new variable (fixed)

Returning a pointer to a local variable is fine in Go

Unlike C, Go's escape analysis moves the variable to the heap automatically.

func newUser() *User {
    u := User{Name: "Alice"} // allocated on heap (escape analysis)
    return &u                // perfectly safe in Go
}

Performance Considerations

Scenario Recommendation Why
Small structs (1–3 fields) Value Copy is cheap, avoids heap allocation
Large structs (many fields) Pointer Avoids expensive copy
Slices, maps, channels Value Already reference types internally
Need mutation Pointer Only way to modify original
Hot path, high allocation rate Value where possible Reduces GC pressure

Escape analysis

Use go build -gcflags="-m" to see what escapes to the heap. Pointer values that escape create GC work — keep hot-path data on the stack when possible.

Interview Tips

Interview Tip

"When should you use a pointer receiver vs a value receiver?" This is the #1 pointer question. Answer:

Use a pointer receiver when:

  • The method needs to mutate the receiver
  • The receiver is a large struct (avoids copy)
  • Consistency — if any method needs a pointer receiver, use it for all

Use a value receiver when:

  • The method is read-only and the struct is small
  • You want the receiver to be safe for concurrent use without locks
  • The type is a small value type like time.Time

Interview Tip

"Does Go have pointer arithmetic?" No. This is a deliberate safety feature. You cannot increment a pointer to walk through memory like in C. This eliminates an entire class of buffer overflow vulnerabilities.

Interview Tip

"Is it safe to return a pointer to a local variable?" Yes. Go's escape analysis detects this and allocates the variable on the heap instead of the stack. The garbage collector manages its lifetime.

Key Takeaways

  • & gets an address, * dereferences it — the only two pointer operations you need.
  • Zero value of a pointer is nil — always guard against nil dereference.
  • No pointer arithmetic — Go trades flexibility for safety.
  • Use pointers for mutation, large structs, and nilability; use values for small immutable data.
  • Prefer &T{...} over new(T) for readability.
  • Be consistent with receiver types across a type's method set.