Go for Reviewers · Lesson 02

Interfaces Are Implicit

No implements. No registration. A type satisfies an interface just by having the methods — and that changes where interfaces live.

Why this, now: In Lesson 1 you saw s.client.ListDApps(...). That client is an interface. To review PRs you must answer two questions fast: "what does this interface require?" and "which concrete type is actually plugged in here?" — and in Go the link between them is invisible.

1 · Three languages, three mental models

You know two of these already. The third is the one to internalize.

Python · duck typing (runtime)
# no declaration at all.
# works if .read() exists
# — discovered when called
def load(src):
    return src.read()
Java · explicit (compile)
class File
  implements Reader {
  // MUST name Reader
  public String read(){...}
}
Go · implicit (compile)
type File struct{}
// no "implements".
func (f *File) Read() string {...}
// satisfies Reader automatically

Go takes the safety of Java (checked at compile time — a type that's missing a method won't build) and the decoupling of Python (the concrete type never mentions the interface). The result:

The rule: a type satisfies an interface if it has all the interface's methods. That's the entire ceremony. There is no keyword, no list, no registration. [Effective Go]

2 · The consequence reviewers must know: interfaces live with the consumer

Because the implementer doesn't name the interface, the interface can be defined wherever it's used, not where the type is built. Go's strong convention — and a live review rule — is exactly that:

Go Code Review Comments: "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values." Don't define an interface next to its implementation just to enable mocking. [source]

This is the opposite of the Java instinct (interface ships with, or above, the implementation). In Go the consumer says "I need something that can do X" and declares a tiny interface for X; any type with that method drops in.

3 · This exact pattern, in your repo

The dapp service consumes; the dapp_storage client implements. They never reference each other's interface name.

CONSUMER · services/dapp/interfaces.go
package dapp

// the service declares ONLY what
// it needs — one method.
type Client interface {
    ListDApps(ctx context.Context,
        limit, offset int64,
    ) ([]*dapp_storage.DApp, error)
}
IMPLEMENTER · clients/dapp_storage/client.go
package dapp_storage

type Client struct{ ... }

// has 5 methods, incl. this one.
// never mentions dapp.Client.
func (c *Client) ListDApps(
  ctx context.Context, limit, offset int64,
) ([]*dapp_storage.DApp, error) { ... }
// GetDAppByID, GetDAppByProtocol,
// CreateDAppByProtocol, ...
*dapp_storage.Client5 methods
→ satisfies →
dapp.Clientneeds 1 method
→ injected at →
NewService(cfg, log, client)in services wiring

Read it like a reviewer:

Where the Python analogy breaks: Python duck typing fails at runtime ("AttributeError: no read"). Go's implicit satisfaction is checked at compile time — a missing method is a build error, not a production surprise. And unlike Python, the set of required methods is written down explicitly in the interface, so a reviewer can see the contract without running anything.

4 · The two reviewer reflexes

Reflex A — "where's the real type?"

When a PR uses s.client.Foo() and client is an interface, the concrete type is wherever NewService / the DI wiring was called. Trace the constructor's argument, not the interface. The interface only tells you the contract; the wiring tells you the behavior.

Reflex B — "is this interface the right size?"

Small interfaces are idiomatic; a single-method interface named with an -er suffix (Reader, Writer, Closer) is the Go ideal. [Effective Go] A PR that introduces a 12-method interface, or defines an interface in the implementer's package "for mocking", is worth a comment.

Reviewer's eye — what to flag:

5 · Drill — read the contract

Instant feedback. For the reps, not a grade.

Q1 Does it satisfy?
Given type Client interface { ListDApps(ctx, limit, offset int64) ([]*DApp, error) }, which type satisfies it?
Q2 Where should it live?
A PR adds a PriceFetcher interface so the portfolio service can be tested. The author put it in clients/price_storage/, next to the real client. What's the idiomatic comment?
Q3 Find the real behavior
Reviewing a diff, you see s.client.ListDApps(ctx, 100, 0) where s.client is of interface type dapp.Client. To know what actually runs, where do you look?
I'm your teacher — ask me anything.
Want to see how the dapp_storage.Client wraps gRPC errors into errorx namespaces (the Error.WrapWithNoMessage(err) / ErrNotFound subtypes at the bottom of client.go)? That's the bridge back to Lesson 1's error chain — ask and I'll walk it. Or ask why method sets differ between *Client and Client (that's the pointer-vs-value receiver topic, queued next).

Terms introduced

Added to your GLOSSARY.md: