Lesson 3 · Foundations
Structs: value vs pointer
What Go copies, what it shares — the semantics everything else rests on.
Your win: predict whether a change inside a function is visible to
the caller, and choose T vs *T (including for receivers) the
way our repo's style rule says to.
The default: everything is copied
In Go, assignment and function calls copy the value. Assign a struct and you get an independent duplicate; pass a struct to a function and it works on a copy — mutations inside are invisible to the caller.1 If you came from a language where objects are implicitly references, this is the single biggest reset.
func deactivate(e Email) { e.IsActive = false } // mutates a COPY
email := Email{IsActive: true}
deactivate(email)
// email.IsActive is still true — the function never touched the original
Pointers: the explicit way to share
A pointer (*T) holds an address, so a callee can reach back and mutate
the original — and you avoid copying a big struct.
func deactivate(e *Email) { e.IsActive = false } // mutates the ORIGINAL
deactivate(&email) // & takes the address; now email.IsActive == false
Receivers are the same rule, for methods
A method's receiver follows identical semantics. A pointer receiver can mutate the value and avoids a copy; a value receiver gets a copy. Our entities use pointer receivers precisely because they mutate or are non-trivial:
internal/spike/modules/email/domain/model/email.go · infrastructure/repositories/email.go:17func (e *Email) FieldMap() (fields []string, values []interface{}) { ... } // pointer
func (repo *EmailRepo) UpsertEmail(ctx, db, e *Email) error { ... } // pointer
// A value receiver, where a copy is fine and the type is tiny:
func (f future) Await() Result { ... } // golibs/asyncawait/async.go:13
.claude/rules/go-code-style.md)
Use a pointer receiver/param when you must mutate in place, the
struct is large, or you need nil to disambiguate ((nil, err))
— or just for consistency across a type's methods. Never use a
pointer to a slice, map, or interface (they're already reference-like). Prefer
[]Book over []*Book.
Constructors hand back pointers
That's why our constructors return *T: the thing is meant to be shared
and mutated, not copied around.
spike/.../grpc/email_modifier_service.go:39
func NewEmailModifierService(...) *EmailModifierService { return &EmailModifierService{...} }
A Tour of Go — "Methods and pointers"
Interactive: flip a receiver between T and *T and watch
the behaviour change. For depth, 100 Go Mistakes §on value vs pointer semantics.
→ go.dev/tour — methods & pointers
→ 100 Go Mistakes (receivers)
Check yourself (from memory)
Q1. You pass a struct (by value) to a func that sets a field. After it returns, the caller's struct is…
*T to affect the caller's value.
Q2. A pointer receiver func (e *Email) ... is chosen mainly to…
Q3. Our style rule says never take a pointer to a…
[]Book to []*Book.
e Email and does e.Status = X. Does the caller see it? What changes with e *Email?e *Email
both refer to the same struct, so the mutation is visible. Copy-by-default is
Go's core value semantics; pointers opt into sharing.T vs *T satisfies an
interface (the method-set rule)? That's Lesson 5 — but ask me for a
preview if it's nagging you.
1. Go spec — calls & parameter passing; A Tour of Go — methods.