On this page
Pointers
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 modifiedPointer 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) // 42Pass 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:
- The method needs to modify the receiver
- The struct is large (avoid costly copies — more than ~64 bytes)
- You need to represent an optional value (nil as "no value")
- Consistency: if any method of a type needs a pointer, make all methods use pointers
Use a value when:
- The type is small (int, string, struct with 1-2 fields)
- The type should not be modified (immutable types)
- 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 = &emailPointers 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.
Sign in to track your progress