Go Pointers with Structs — Junior Level¶
1. Introduction¶
What is it?¶
A pointer to a struct (*StructType) is one of Go's most common patterns. It lets you: - Mutate struct fields from inside a function or method. - Share a single struct between multiple variables/functions. - Build linked structures (lists, trees). - Avoid copying large structs.
type Point struct{ X, Y int }
p := &Point{X: 1, Y: 2}
p.X = 99 // Go auto-dereferences: same as (*p).X = 99
fmt.Println(p) // &{99 2}
2. Prerequisites¶
- Pointers basics (2.7.1)
- Structs (2.3.5)
- Methods (intro)
3. Glossary¶
| Term | Definition |
|---|---|
*StructType | Pointer to a struct |
| Auto-dereference | p.field works without explicit * |
| Constructor | Function returning *T |
| Pointer receiver | Method receiver of type *T |
| Field address | &p.field — pointer to a single field |
| Linked structure | Data structure where nodes hold pointers to other nodes |
4. Core Concepts¶
4.1 Allocating¶
&CompositeLiteral{...} is the most common form.
4.2 Auto-Dereference¶
You don't need (*p) syntax — Go inserts it.
4.3 Pointer Receivers¶
func (p *Point) Translate(dx, dy int) {
p.X += dx
p.Y += dy
}
p := &Point{X: 0, Y: 0}
p.Translate(5, 10)
fmt.Println(p) // &{5 10}
The method mutates *p (same struct as caller).
4.4 Calling Pointer Methods on Values¶
p := Point{X: 1, Y: 2} // value, not pointer
p.Translate(1, 1) // Go takes &p automatically
fmt.Println(p) // {2 3}
Works only if p is addressable.
4.5 Returning Pointer-to-Struct (Constructor)¶
Idiomatic factory pattern.
5. Real-World Analogies¶
A house key: many people can have a copy of the key; they all access the same house. Mutations (rearranging furniture) are visible to all.
A medical record: multiple doctors can hold a pointer to the same patient record. Updates by one are visible to others.
6. Mental Models¶
caller variable: p (*Point)
│
▼
Memory at some address: { X: 99, Y: 2 } ← the actual Point struct
▲
│
function parameter q (*Point) — same address — same struct
Pointer-to-struct = address of where the struct lives.
7. Pros & Cons¶
Pros¶
- Mutation across function calls
- Sharing without copying
- Foundation for linked structures
- Method receivers can mutate
Cons¶
- Nil dereference panics
- Aliasing complications
- Indirection cost (small)
8. Use Cases¶
- Mutator methods (
p.SetName(...)) - Constructors (
NewT(...) *T) - Linked lists, trees, graphs
- Shared state
- Avoiding copies of large structs
9. Code Examples¶
Example 1 — Simple¶
type User struct{ Name string }
func rename(u *User, name string) {
u.Name = name
}
u := &User{Name: "Old"}
rename(u, "Ada")
fmt.Println(u.Name) // Ada
Example 2 — Pointer Methods¶
type Counter struct{ N int }
func (c *Counter) Inc() { c.N++ }
c := &Counter{}
c.Inc(); c.Inc(); c.Inc()
fmt.Println(c.N) // 3
Example 3 — Linked List¶
type Node struct {
V int
Next *Node
}
head := &Node{V: 1, Next: &Node{V: 2, Next: &Node{V: 3}}}
for n := head; n != nil; n = n.Next {
fmt.Println(n.V)
}
Example 4 — Pointer to Struct Field¶
Example 5 — Constructor¶
type Server struct{ Addr string; Port int }
func NewServer(addr string, port int) *Server {
return &Server{Addr: addr, Port: port}
}
s := NewServer("localhost", 8080)
fmt.Printf("%+v\n", s)
10. Coding Patterns¶
Pattern 1 — Constructor¶
Pattern 2 — Builder¶
func (s *Server) WithAddr(a string) *Server { s.Addr = a; return s }
s := NewServer().WithAddr(":9000").WithPort(443)
Pattern 3 — Self-Referential¶
Pattern 4 — Optional Field¶
11. Clean Code Guidelines¶
- Use
&T{...}for initialized allocation. - Use pointer receivers for mutating methods.
- Be consistent: if any method on T uses pointer receiver, all should.
- Always nil-check at API boundaries.
- Constructors return
*Tfor types with methods.
12. Product Use / Feature Example¶
A bank account with mutating operations:
type Account struct {
Balance int
}
func NewAccount(initial int) *Account {
return &Account{Balance: initial}
}
func (a *Account) Deposit(amount int) { a.Balance += amount }
func (a *Account) Withdraw(amount int) {
if amount > a.Balance {
panic("insufficient funds")
}
a.Balance -= amount
}
a := NewAccount(100)
a.Deposit(50)
a.Withdraw(30)
fmt.Println(a.Balance) // 120
13. Error Handling¶
func (a *Account) WithdrawSafe(amount int) error {
if a == nil {
return fmt.Errorf("nil account")
}
if amount > a.Balance {
return fmt.Errorf("insufficient")
}
a.Balance -= amount
return nil
}
14. Security Considerations¶
- Nil-check pointers from external sources.
- Don't expose internal pointers if callers shouldn't mutate.
- Defensive copy when storing caller-provided pointers.
15. Performance Tips¶
- Pointer pass: 8 B (free).
- Value pass small struct (≤ 64 B): also free (registers).
- Value pass large struct: expensive copy.
- Pointer dereference: 1-2 cycles.
For large structs, prefer pointers.
16. Metrics & Analytics¶
type Sample struct{ Name string; Value float64 }
func (s *Sample) Record() {
fmt.Printf("[%s] %f\n", s.Name, s.Value)
}
17. Best Practices¶
- Pointer receivers for mutating methods.
- Consistent receiver type per type.
- Use
&T{...}for allocation + initialization. - Always nil-check.
- Document what nil means for pointer fields.
18. Edge Cases & Pitfalls¶
Pitfall 1 — Nil Dereference¶
Pitfall 2 — Pointer Receiver on Map Value¶
Store pointers: map[string]*Counter.
Pitfall 3 — Address of Loop Variable (Pre 1.22)¶
Pitfall 4 — Returning Pointer to Local¶
Safe (escape analysis), but understand it allocates.
Pitfall 5 — Mixing Receiver Types¶
type T struct{}
func (t T) A() {}
func (t *T) B() {}
// T satisfies interface {A()} but not {A(); B()}
19. Common Mistakes¶
| Mistake | Fix |
|---|---|
Forgetting & for constructor | &T{...} |
| Mixing receiver types | Be consistent |
| Pointer method on map value | Use map[K]*V |
| Nil dereference | Check first |
20. Common Misconceptions¶
1: "Always use pointer-to-struct for performance." Truth: For small structs, value pass is fine.
2: "new(Point) is different from &Point{}." Truth: Equivalent for zero-initialization.
3: "Auto-dereference works for any operator." Truth: Only for .field and method calls. *p syntax still needed for explicit dereference in expressions.
21. Tricky Points¶
- Pointer receivers can be called on addressable values.
- Field access through pointer is auto-dereferenced.
- Pointers to struct fields are valid (
&p.X). - Embedded pointer fields enable composition.
- Self-referential structs require pointer field.
22. Test¶
type Counter struct{ N int }
func (c *Counter) Inc() { c.N++ }
func TestInc(t *testing.T) {
c := &Counter{}
c.Inc(); c.Inc()
if c.N != 2 {
t.Errorf("got %d, want 2", c.N)
}
}
23. Tricky Questions¶
Q1: What does this print?
A:2. t is addressable; Go takes &t automatically. Q2: Will this compile?
A: No. Map value not addressable.24. Cheat Sheet¶
// Allocate
p := &T{...}
p := new(T)
// Auto-deref
p.field = ...
p.method()
// Pointer to field
fp := &p.field
// Constructor
func New() *T { return &T{...} }
// Pointer receiver
func (t *T) Mutate() { t.field = ... }
25. Self-Assessment Checklist¶
- I can allocate
&Struct{} - I use auto-dereference
- I write pointer receiver methods
- I write constructors
- I build linked structures
- I nil-check at boundaries
26. Summary¶
*Struct is the bridge between functions and shared/mutable struct state. Use &T{...} to allocate. Auto-dereference makes p.field and p.Method() work. Use pointer receivers for mutation. Required for self-referential types. Always nil-check.
27. What You Can Build¶
- Object-style data types
- Linked lists, trees
- Constructors
- Builders
- Caches and registries
28. Further Reading¶
29. Related Topics¶
- 2.7.1 Pointers Basics
- 2.7.3 With Maps & Slices
- Chapter 3 Methods