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.

In plain English Plain English: passing a struct normally makes a copy. Passing `*T` lets multiple functions talk about the same value instead of independent copies.

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.
Backend use case Backend use case: request DTOs are often fine as values, but repositories and mutable domain objects often travel as pointers so methods can update shared state or avoid copying larger structs.
Common mistake Common mistake: thinking “Go passes objects by reference.” It does not. The senior answer is “Go always passes values; sometimes the value is a pointer.”
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.