Hook
Interfaces in Go feel almost invisible compared to languages like Java or C#—there's no explicit implements keyword, and that's by design. Go interfaces explained: real-world lessons and examples reveals why this simplicity is actually one of Go's most powerful features for building maintainable, testable production APIs. Once you understand how to use them correctly, you'll write code that's easier to refactor, mock, and compose.
Implicit implementation — satisfaction through structure
In Go, a type satisfies an interface by implementing all of its methods. There's no declaration, no inheritance chain, no boilerplate. If your struct has a method that matches the interface signature, you're done.
This is radically different from many languages. Consider a real scenario: you're building a payment service API and you have a PaymentProcessor type. You also have an interface that describes what payment processors should do:
package payment
type PaymentProcessor interface {
Process(ctx context.Context, amount float64) (transactionID string, err error)
Refund(ctx context.Context, transactionID string) error
}
type StripeProcessor struct {
apiKey string
}
func (p *StripeProcessor) Process(ctx context.Context, amount float64) (string, error) {
// Call Stripe API
return "txn_123", nil
}
func (p *StripeProcessor) Refund(ctx context.Context, transactionID string) error {
// Call Stripe refund endpoint
return nil
}
You never wrote type StripeProcessor implements PaymentProcessor. Go simply recognizes that StripeProcessor has both methods with matching signatures. Later, if you add a PayPalProcessor with the same methods, it also satisfies PaymentProcessor—without touching the interface or either implementation.
This implicit satisfaction means your codebase stays loose. You can introduce interfaces after the fact, apply them to existing types, and let third-party packages satisfy your interfaces without them knowing about your code. In a microservices world where you're integrating with external libraries, that's invaluable.
Small interfaces — the power of composition
The Go standard library teaches us an important lesson: interfaces should be small. The most famous example is io.Reader, which has exactly one method:
type Reader interface {
Read(p []byte) (n int, err error)
}
One method. That's it. And because it's so simple, it composes everywhere. Any type that can read data—a file, a network connection, a buffer, a gzipped stream, even a mock for testing—can be used anywhere io.Reader is accepted. The standard library is built on this: io.Copy, bufio.Scanner, json.Decoder`, all work with anything that implements Read.
In your production APIs, apply the same principle. Rather than defining a giant interface with 15 methods, break it into smaller, focused contracts. Let's revisit the payment scenario:
// Too big
type PaymentService interface {
Process(ctx context.Context, amount float64) (string, error)
Refund(ctx context.Context, txID string) error
ValidateCard(ctx context.Context, card string) error
GetBalance(ctx context.Context) (float64, error)
UpdateWebhook(ctx context.Context, url string) error
}
// Better: small, focused interfaces
type Processor interface {
Process(ctx context.Context, amount float64) (string, error)
}
type Refunder interface {
Refund(ctx context.Context, txID string) error
}
type Validator interface {
ValidateCard(ctx context.Context, card string) error
}
Now your handlers can ask for exactly what they need. The order creation handler accepts Processor. The refund handler accepts Refunder. This makes testing simpler—you mock only the methods you care about—and future changes to validation logic don't affect code that doesn't validate.
We've seen teams in production e-commerce platforms struggle when they define monster interfaces. Small interfaces reduce coupling. They let you test functions in isolation. They let you swap implementations with minimal friction.
Define at the consumer — interfaces live where they're used
This is where Go interface design departs most sharply from traditional OOP. You don't define interfaces in the package where the concrete type lives. You define them in the package that uses them.
Say you have a database package with an Order struct and methods to fetch and store orders. In a different package—your API handlers—you define the interface you actually need:
// Package store - defines the Order type and persistence logic
package store
type Order struct {
ID string
Total float64
Status string
}
func (db *Database) GetOrder(ctx context.Context, id string) (Order, error) {
// Fetch from DB
return Order{}, nil
}
func (db *Database) SaveOrder(ctx context.Context, order Order) error {
// Write to DB
return nil
}
// Package handlers - defines interfaces for what it needs
package handlers
import (
"context"
"yourapp/store"
)
type OrderStore interface {
GetOrder(ctx context.Context, id string) (store.Order, error)
}
type OrderHandler struct {
orders OrderStore
}
func (h *OrderHandler) FetchOrder(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
order, err := h.orders.GetOrder(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(order)
}
Notice: the handlers package defines OrderStore—not the store package. The store package has no idea it's being used via an interface. This inversion lets you:
- Keep packages independent. The store doesn't depend on handlers' interface definitions.
- Mock easily in tests. In your test package, you write a tiny fake
OrderStoreimplementation. - Support multiple implementations. Your
OrderHandlerworks with a realDatabase, a cache layer, or a test double—all without the handler knowing which one.
This pattern avoids the "interface bloat" problem we see in monolithic e-commerce platforms. When the database team adds a new method to their Database struct, handlers that don't need it aren't affected. Only handlers that use the new method need to update their local interface definition.
Accept interfaces, return structs — flexible APIs, concrete contracts
This is Go's most important guideline for API design. Your function signatures should accept interfaces but return concrete types.
// Good: accept an interface, return a struct
func CreateOrder(ctx context.Context, store OrderStore, items []Item) (*Order, error) {
order := &Order{
ID: generateID(),
Items: items,
}
if err := store.SaveOrder(ctx, order); err != nil {
return nil, err
}
return order, nil
}
// Less flexible: return an interface
func CreateOrder(ctx context.Context, store OrderStore, items []Item) OrderLike, error {
// Caller can't know what concrete type they got back
// Can't call type-specific methods
}
Why? When you return a struct, callers know exactly what they have and can call any method on it. When you return an interface, callers can only call methods on the interface—and if they need more, they're stuck type-asserting or requesting a new interface.
In production CRM systems, we've seen this pattern prevent API versioning chaos. New code gets a new version of the struct (e.g., OrderV2), and old code keeps working with Order`. Both satisfy the same OrderStore interface the handlers care about.
The reason it works: accepting interfaces means your function is flexible about where its data comes from. Returning structs means your function is honest about what it produces. Your callers can depend on the structure and evolve their code without being blocked by interface changes.
Common mistakes
Mistake 1: Defining interfaces too early. Don't create an interface before you have at least two implementations or a real need to mock. This leads to unused abstractions that clutter your codebase. Write the concrete type first. When you need to swap implementations or test, define the interface then.
Mistake 2: Making interfaces too large. A 10-method interface tempts you to mock all 10 methods in tests, even when you only care about 2. This test bloat hides what you're actually testing. If you need 10 methods, split the interface into smaller ones.
Mistake 3: Defining interfaces in the wrong package. New Go developers often put interfaces in the same package as the concrete type, like in their store package. This creates a dependency from store (the domain) back to handlers (a consumer). Invert it: handlers define the interfaces it needs.
Mistake 4: Returning interfaces instead of structs. This breaks discoverability. Callers can't see what fields or methods are available without reading your docs or code. It also makes it harder to extend the API later.
Key takeaways
- Go interfaces are satisfied implicitly—if a type has all the methods, it satisfies the interface, with no explicit declaration required.
- Small interfaces (one to three methods) compose well and are easier to test and mock than large ones.
- Define interfaces in the package where they're used, not where the concrete types are defined. This keeps packages loosely coupled.
- Accept interfaces in function parameters for flexibility; return concrete structs so callers know exactly what they have.
- Don't create interfaces speculatively. Wait until you have multiple implementations or a genuine testing need.






Comments
Loading comments…