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.

value (copy) pointer (shared) ──────────── ──────────────── caller: Email{Active:true} caller: Email{Active:true} ◀─┐ callee: Email{Active:true} (copy) callee: *Email ──────────────┘ mutation stays local mutation hits the original
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:17
func (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
Our repo's rule (from .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{...} }
Interview trap ahead (Lesson 4) "Everything is copied" has an asterisk: slices, maps, and channels are copied as headers that still point at shared data. So copying a slice and then mutating an element is visible to the other copy. We unpack that next — it's the most common Go bug.
Do this next

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…

Pass-by-value copies the whole struct; the func mutates its copy. Pass *T to affect the caller's value.

Q2. A pointer receiver func (e *Email) ... is chosen mainly to…

Mutation and copy-avoidance. Our entity/repo methods use pointer receivers for exactly these reasons.

Q3. Our style rule says never take a pointer to a…

Slices/maps/interfaces are already reference-like; a pointer to them adds confusion, not sharing. Prefer []Book to []*Book.
A func takes e Email and does e.Status = X. Does the caller see it? What changes with e *Email?
recall, then click to reveal
No — the func received a copy of the struct, so mutating the copy leaves the caller's value untouched. With 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.
Want the mechanical rule for when 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.