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
Interview Prep
Practice concise answers, then expand each card for the explanation.
1BeginnerQuestionExplicit vs implicit interface satisfaction?+
Answer
2BeginnerQuestionnil interface gotcha?+
Answer
3IntermediateQuestionAccept interfaces return structs?+
Answer
4IntermediateQuestionType assertion vs type switch?+
Answer
5AdvancedQuestionDesign notification system with Email, SMS, Push — add Slack without caller change.+
Answer
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.