Go Pointers — & * nil new and Pointer Receivers Explained
In this tutorial, you will learn about Go Pointers. We cover key concepts, practical examples, and best practices to help you master this topic.
Go pointers hold memory addresses using & to get an address and * to dereference or declare pointer types, with nil representing an uninitialized pointer and pointer receivers for modifying receivers.
What You'll Learn
- Creating and using pointers
- The & and * operators
- nil pointers and safety
- Pointer receivers in methods
Why It Matters
Pointers enable efficient passing of large structs and modification of values in functions. Docker uses pointers for optional configuration fields. Kubernetes API uses pointers for nullable fields. DodaZIP uses pointers for optional compression parameters.
Real-World Use
A parser function returns a pointer to a parsed struct (or nil on failure). Configuration structs use pointers for optional fields. Large data structures passed as pointers avoid copying.
flowchart LR
A["Pointers"] --> B["& and *"]
B --> C["nil"]
C --> D["Pointer Receivers"]
D --> E["Methods"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Pointer Operations
package main
import "fmt"
func main() {
x := 42
p := &x // p is a pointer to x
fmt.Println(p) // 0xc0000b2008 (memory address)
fmt.Println(*p) // 42 (dereference — get value)
*p = 100 // Change x through pointer
fmt.Println(x) // 100
}
Pointer Types
var p *int // nil pointer to int
var q *string // nil pointer to string
x := 42
p = &x // Now p points to x
Pointers in Functions
func zeroValue(x int) {
x = 0 // Modifies copy
}
func zeroPointer(x *int) {
*x = 0 // Modifies original
}
func main() {
n := 10
zeroValue(n)
fmt.Println(n) // 10 (unchanged)
zeroPointer(&n)
fmt.Println(n) // 0 (changed)
}
The new Function
new(T) allocates zeroed memory and returns a pointer:
p := new(int)
fmt.Println(*p) // 0
*p = 42
fmt.Println(*p) // 42
// Equivalent to:
var x int
p = &x
nil Pointers
var p *int
fmt.Println(p) // nil
// fmt.Println(*p) // Panic! nil pointer dereference
// Safe access
if p != nil {
fmt.Println(*p)
}
Pointer Receivers
Methods can have pointer receivers to modify the struct:
type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++
}
func (c *Counter) Value() int {
return c.value
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.Value()) // 2
}
When to Use Pointer vs Value Receiver
type LargeStruct struct {
// Many fields...
}
// Pointer receiver — modify struct or avoid copying
func (ls *LargeStruct) Modify() {
// Can modify fields
}
// Value receiver — read-only, copy is OK for small types
func (ls LargeStruct) ReadOnly() string {
return "read only"
}
Pointer to Struct
type Person struct {
Name string
Age int
}
func main() {
// Create pointer to struct
p := &Person{Name: "Alice", Age: 25}
fmt.Println(p.Name) // Go automatically dereferences (p).Name
// Without & — value
p2 := Person{Name: "Bob", Age: 30}
p3 := &p2 // Pointer to p2
p3.Name = "Charlie"
fmt.Println(p2.Name) // Charlie (modified through pointer)
}
Function Arguments
func updateName(p *Person, name string) {
p.Name = name
}
func main() {
p := &Person{Name: "Alice"}
updateName(p, "Bob")
fmt.Println(p.Name) // Bob
}
Common Mistakes
1. Nil Pointer Dereference
var p *int
*p = 42 // Panic!
// Always check
if p != nil {
*p = 42
}
2. Returning Pointer to Local Variable (Actually OK in Go)
func createPerson() *Person {
p := Person{Name: "Alice"} // Go allocates on heap if needed
return &p // This is safe in Go!
}
3. Using Pointer When Value Would Do
// Unnecessary pointer — small value type
func (c *Counter) Value() *int {
return &c.value
}
// Better — return value
func (c *Counter) Value() int {
return c.value
}
4. Confusing * (type) and * (dereference)
var p *int // *int means "pointer to int"
*p = 42 // *p means "value at pointer p"
5. Not Initializing Maps/Slices in Structs
type Config struct {
Settings map[string]string
}
c := Config{}
c.Settings["key"] = "value" // Panic — nil map
Practice Questions
1. What does & do in Go?
& returns the memory address of a variable. It creates a pointer to the variable. x := 5; p := &x makes p point to x.
2. What does * do for pointers?
* has two uses: In a type declaration (*int means "pointer to int"), and as a dereference operator (*p gets the value at the pointer).
3. What is a nil pointer?
A pointer that doesn't point to any memory location. Dereferencing a nil pointer causes a runtime panic.
4. When should you use a pointer receiver vs value receiver?
Use pointer receiver when you need to modify the struct, when the struct is large (avoid copying), or for consistency. Use value receiver for small, immutable types.
Challenge: Write a function that swaps two integer values using pointers.
Solution
package main
import "fmt"
func swap(a, b *int) {
*a, *b = *b, *a
}
func main() {
x, y := 10, 20
fmt.Printf("Before: x=%d, y=%d\n", x, y)
swap(&x, &y)
fmt.Printf("After: x=%d, y=%d\n", x, y)
}
Expected output:
Before: x=10, y=20
After: x=20, y=10
FAQ
{{< faq question="Does Go have pointer arithmetic?" >}} No. Unlike C/C++, Go doesn't allow pointer arithmetic (adding/subtracting from pointers). This prevents a class of memory safety bugs. {{< /faq >}}
{{< faq question="Are maps and slices pointers?" >}} No, but they contain internal pointers to underlying data. This is why maps and slices appear to be reference types when passed to functions. {{< /faq >}}
{{< faq question="What's the difference between new and make?" >}
new(T) returns a pointer to a zero-initialized T. make(T, args) initializes slices, maps, and channels (types that need internal setup). new allocates; make initializes.
{{< /faq >}}
{{< faq question="Can I return a pointer to a local variable?" >}} Yes. Go's escape analysis determines if a variable escapes the function scope and allocates it on the heap if needed. This is safe and idiomatic. {{< /faq >}}
{{< faq question="Should I use pointers for all function parameters?" >}} No. Use pointers when you need to modify the argument, when the type is large, or when nil is a meaningful value. For small types (int, bool, float64), pass by value. {{< /faq >}}
Try It Yourself
package main
import "fmt"
type Account struct {
Balance float64
}
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func (a *Account) Withdraw(amount float64) bool {
if a.Balance < amount {
return false
}
a.Balance -= amount
return true
}
func main() {
acc := Account{Balance: 100}
fmt.Printf("Initial: $%.2f\n", acc.Balance)
acc.Deposit(50)
fmt.Printf("After deposit: $%.2f\n", acc.Balance)
if acc.Withdraw(30) {
fmt.Printf("After withdrawal: $%.2f\n", acc.Balance)
}
fmt.Printf("Pointer: %p\n", &acc)
fmt.Printf("Balance via pointer: $%.2f\n", (&acc).Balance)
}
Expected output:
Initial: $100.00
After deposit: $150.00
After withdrawal: $120.00
Pointer: 0xc0000b2008
Balance via pointer: $120.00
What's Next
Now that you understand pointers, learn about methods with value and pointer receivers.
| Topic | Description | Link |
|---|---|---|
| Go Methods | Receiver types, value vs pointer | {{< ref "10-methods" >}} |
| Go Interfaces | Implicit satisfaction, type assertion | {{< ref "11-interfaces" >}} |
| Rust Ownership | Compare Rust Ownership model | Rust |