Lesson 6 · Methods, interfaces & generics

Interfaces

Implicit, small, and declared where they're used — Go's superpower.

Your win: explain implicit satisfaction, argue why Go interfaces are small, and read our repo's "define the interface at the consumer" pattern — the thing that makes the code so mockable.

Satisfaction is implicit

There is no implements keyword. A type satisfies an interface simply by having its methods.1 The implementer doesn't name — or even import — the interface. That one design choice is what decouples Go packages so cleanly.

type Stringer interface { String() string }

// Email satisfies Stringer just by having String() — no declaration needed.
func (e *Email) String() string { return e.EmailID.String }

Keep interfaces small

The proverb "The bigger the interface, the weaker the abstraction."2 Go's most reused interfaces have one method — io.Reader, io.Writer, fmt.Stringer. Small interfaces are easy to satisfy, easy to mock, and compose freely.

Define the interface where you consume it

The idiom that surprises newcomers: in Go you usually declare an interface next to the caller that needs it, listing only the methods that caller uses — not next to the implementation. "Accept interfaces, return structs."

Our controllers do exactly this — an inline, anonymous interface holding just the one method the handler calls:

internal/spike/modules/email/controller/grpc/email_modifier_service.go:25-30
type EmailModifierService struct {
    EmailCommandHandler interface {
        CreateEmail(ctx context.Context, p *commands.CreateEmailPayload) (*dto.Email, error)
    }
}

At the hexagonal seams we also keep segregated port files — a named interface per repository the domain needs:

internal/spike/modules/email/infrastructure/repo_port.go:11
type EmailRepo interface {
    UpsertEmail(ctx context.Context, db database.QueryExecer, e *model.Email) error
    ...
}
Why this makes testing trivial Because the handler depends on a tiny, local interface, a test supplies a mock of just those methods (MockEmailRepo{ mock.Mock }). The concrete &repositories.EmailRepo{} satisfies the port implicitly — it never imports the interface. Swap real for mock at the constructor; nothing else changes. (Mocking is Part 5.)

Accept interfaces, return structs

Functions take interface parameters (so callers can pass anything satisfying them) but constructors return concrete *T (so callers get the full type). You saw this in Lesson 3: NewEmailModifierService(...) *EmailModifierService. This keeps flexibility at the boundary without hiding the real type from its owner.

Do this next

Effective Go — Interfaces

The canonical treatment of implicit satisfaction and interface design. Pair with the Tour's interface section and the Go Proverbs.

go.dev/doc/effective_go#interfaces
A Tour of Go — interfaces

Check yourself (from memory)

Q1. A Go type satisfies an interface when it…

Implicit satisfaction — just have the methods. No keyword, no import of the interface by the implementer.

Q2. "The bigger the interface, the weaker the abstraction" argues for…

Smaller = easier to satisfy, mock, and compose. The most-used Go interfaces have one method.

Q3. Our controllers declare their repo interfaces…

Consumer-side, listing only the methods used — which is what makes them minimal and trivial to mock.
Why does defining an interface at the consumer (not the implementer) help coupling and testing?
recall, then click to reveal
The consumer lists only the methods it actually needs → the interface stays minimal. The implementer satisfies it implicitly and never imports it → packages stay decoupled. And a test can supply a small mock of just those methods. "Accept interfaces, return structs."
Want the counter-case — when a named, shared interface (like our port files) beats an inline one? Ask me; it's a nuanced design call.

1. Effective Go — interfaces; Go spec — interface types.

2. Go Proverbs.