Interfaces
Interfaces define behavior contracts — a set of method signatures.
Introduction
Interfaces define behavior contracts — a set of method signatures. Go interfaces are satisfied implicitly: no implements keyword. This enables decoupling, testability with mocks, and the accept-interfaces-return-structs API design principle.
The empty interface any (Go 1.18+) accepts all types — use sparingly. Small interfaces (io.Reader, io.Writer with one method) are idiomatic. Understanding nil interface vs nil concrete value in interface is a classic interview trap.
Interfaces power dependency injection, HTTP handlers, database drivers, and plugin architectures throughout the Go ecosystem.
The story
Google's internal RPC layers depend on small interfaces: an io.Reader for streaming uploads, a StorageBackend with Put and Get for swapping GCS mocks in tests. Uber's dependency injection wires concrete implementations behind interfaces so integration tests replace Kafka with an in-memory bus.
Go interfaces are satisfied implicitly — no implements keyword — enabling decoupling without inheritance hierarchies that plague larger codebases.
Understanding the topic
Key concepts
- type Reader interface { Read(p []byte) (n int, err error) }.
- Implicit satisfaction — no declaration needed on implementing type.
- Interface value holds (type, value) pair — nil interface vs typed nil.
- Empty interface any — accepts everything; prefer generics or union constraints.
- Type assertion: v := i.(ConcreteType) or v, ok := i.(ConcreteType).
- Type switch on interface values for polymorphic dispatch.
classDiagramclass io.Reader {<<interface>>+Read(p []byte)}class os.File {+Read(p []byte)}io.Reader <|.. os.File
Step-by-step explanation
- Define interface with method signatures.
- Concrete type implementing all methods satisfies interface automatically.
- Assign concrete to interface variable — dynamic dispatch at runtime.
- Interface with nil concrete value is not nil interface if type set.
- Compile-time check: var _ MyInterface = (*MyType)(nil).
- Embed interfaces in interfaces for composition.
Practical code example
Payment processor interface with mock implementation for testing:
package mainimport "fmt"type PaymentProcessor interface {Charge(amount float64, currency string) (string, error)}type StripeProcessor struct{}func (StripeProcessor) Charge(amount float64, currency string) (string, error) {return fmt.Sprintf("stripe_ch_%f_%s", amount, currency), nil}type MockProcessor struct{}func (MockProcessor) Charge(amount float64, currency string) (string, error) {return "mock_charge_id", nil}func checkout(processor PaymentProcessor, amount float64) {id, err := processor.Charge(amount, "USD")if err != nil {fmt.Println("failed:", err)return}fmt.Println("charged:", id)}func main() {checkout(StripeProcessor{}, 99.99)checkout(MockProcessor{}, 1.00)var _ PaymentProcessor = StripeProcessor{}}
Line-by-line code explanation
type StorageBackend interface { Put(key string, data []byte) error }defines a minimal contract.type GCSBackend struct{}implements the interface by having matching methods — no declaration needed.func Upload(b StorageBackend, key string, data []byte)accepts any implementation.interface{}oranyholds values of any type — use sparingly; prefer generics or concrete types.type ReadWriter interface { io.Reader; io.Writer }embeds interfaces to compose larger contracts.nil interface pitfall— a typed nil pointer stored in an interface is not equal to nil.var _ StorageBackend = (*MockStorage)(nil)compile-time assertion that MockStorage implements the interface.type Stringer interface { String() string }— fmt printing uses this ubiquitous interface.
Key takeaway: var _ Interface = Type{} compile-time assertion. Small interfaces enable easy mocking. Accept PaymentProcessor interface in checkout function.
Real-world use
Where you'll use this in production
- Repository interfaces for database swap in tests.
- io.Reader/io.Writer streaming in HTTP and file processing.
- http.Handler ServeHTTP for middleware chains.
- Plugin systems loading implementations at runtime.
Best practices
- Keep interfaces small — 1-3 methods (interface segregation).
- Define interfaces at consumer, not producer side.
- Accept interfaces, return concrete types in public APIs.
- Use compile-time assertion: var _ IF = (*T)(nil).
- Avoid any — use generics or specific interfaces.
- Document expected behavior and error semantics.
Common mistakes
- Returning nil concrete pointer in interface — not equal to nil interface.
- Giant interfaces with 10+ methods — hard to mock and implement.
- Defining interfaces before concrete types — consumer-driven interfaces.
- Type assertion without ok — panics on wrong type.
- Overusing any losing type safety.
Advanced interview questions
Q1BeginnerExplicit vs implicit interface satisfaction?
Q2Beginnernil interface gotcha?
Q3IntermediateAccept interfaces return structs?
Q4IntermediateType assertion vs type switch?
Q5AdvancedDesign notification system with Email, SMS, Push — add Slack without caller change.
Summary
Interfaces define behavior; satisfaction is implicit. Small consumer-defined interfaces enable testing and decoupling. Beware nil interface vs typed nil pointer in interface. Type assertions and switches extract concrete types safely. Next lesson: pointers — explicit memory addresses.