In L3 you learned to read *. Now the why: when Go copies, and why it decides the receiver type.
In Python, a variable is always a reference to an object. Passing it to a function, assigning it, sticking it in a list — all share the same object. There is no "copy unless you ask."
Go is the opposite by default. Assignment and function calls copy the value. Pass a struct to a function
and the function gets its own copy; mutating it does nothing to the caller's. A pointer (*T) is how you opt
out of copying — you hand over the address so the callee touches the original.
def bump(a):
a.count += 1 # mutates caller's obj
acct = Account()
bump(acct)
# acct.count is now 1
func bump(a Account) { // gets a COPY
a.count++ // mutates the copy
}
acct := Account{}
bump(acct)
// acct.count is STILL 0 — bug!
func bump(a *Account) { // gets the ADDRESS
a.count++ // mutates the original
}
bump(&acct) // acct.count is now 1 ✓
bump(a Account) compiles and runs. No error.
It just silently does nothing to the caller. Your Python brain reads it as "mutates the account" — Go reads it as "mutates
a throwaway copy". This is the bug to train your eye for.
A method's receiver (the (s *Service) part) follows the exact
same rule. A value receiver gets a copy of the type; a pointer receiver gets the address.
func (a Account) Deposit(n int) {
a.balance += n // changes the COPY
}
// acct.Deposit(100) → balance unchanged
func (a *Account) Deposit(n int) {
a.balance += n // changes the ORIGINAL
}
// acct.Deposit(100) → balance += 100 ✓
sync.Mutex / other no-copy field — use a pointer receiver. Use a value receiver
only for small, immutable, basic-like types. When in doubt, use a pointer.
[Code Review Comments]
The codebase follows the rule precisely — 72 *Service pointer receivers, value receivers only on tiny types.
type Address string // tiny, immutable
func (a Address) String() string {
return string(a)
}
func (a Address) Network() string {
// reads a, never mutates
...
}
type Service struct { // big, has caches
config *Config
client Client
cache *dAppsCache
}
func (s *Service) GetCachedByID(...) {
// shares one Service; no copying
}
Why these choices: Address is a one-word string — copying is free and it's never mutated, so a value receiver
is clean (and means you can call .String() on a plain Address without taking its address).
Service holds caches and config pointers — you want every method working on the same instance, and
copying it per call would be wasteful and wrong. [Effective Go]
This is where pointers-vs-values collides with Lesson 2. The rule:
| If methods are on… | then these satisfy an interface |
|---|---|
value receiver (a T) | both T and *T |
pointer receiver (a *T) | only *T — a plain T does not satisfy it |
So if dapp_storage.Client's methods are on *Client (they are), then *Client satisfies
the dapp.Client interface but a bare Client value does not. This is why you almost always see
&Client{...} / NewClient() returning *Client passed into constructors — the
pointer is what carries the full method set.
cannot use Client{} (value of type Client) as dapp.Client value: Client does not implement dapp.Client (method ListDApps has pointer receiver)
— that compile error is this rule firing. When a teammate's PR hits it, you now know the fix: pass &Client{}, not Client{}.
Mutex/WaitGroup/other no-copy field · type already has any pointer-receiver method (consistency) · default when unsure.(a T) SetX(...) writing a.x = ....sync.Mutex field used with value receivers — copying the mutex breaks locking. [rule]&.Instant feedback. Reps, not grades.
func (a Account) Withdraw(n int) { a.balance -= n }
// caller:
acct.Withdraw(50)After this, what is acct.balance (started at 100)?Address uses a value receivertypes/address.go, func (a Address) String() string uses a value receiver. Best justification?*Client has the method ListDApps (pointer receiver). A PR does var c dapp.Client = Client{}. What happens?sync.Mutex field. A teammate writes its methods with value receivers. Comment?acct.Deposit() works without &)? Ask — both are common review confusions.
Added to your GLOSSARY.md:
(s *T) shares the original; (s T) gets a copy.*T.