Go Interfaces
An interface in Go is a set of method signatures. Any type that has those methods satisfies the interface automatically, with no implements keyword and no declared relationship. A *os.File, a *bytes.Buffer, a network connection, and a gzip stream are all io.Readers simply because each has a Read([]byte) (int, error) method.
This implicit, structural style shapes how Go programs are designed. Interfaces are small, defined by the code that uses them rather than the code that implements them, and composed from one another. It's what makes Go code easy to test and easy to plug together without frameworks.
TL;DR
- A type satisfies an interface by having its methods. Satisfaction is implicit and checked at compile time.
- Keep interfaces small: one to three methods is typical (
io.Reader,io.Writer,fmt.Stringer,error). - Define interfaces where they're consumed, not next to the implementation.
- Accept interfaces, return concrete types (structs).
- Recover the concrete type with a type assertion (
v, ok := x.(T)) or a type switch. - An interface holding a nil pointer is not nil, which is the most common interface bug.
Quick Example
Consumer-defined interfaces make code testable without mocking frameworks:
The SES client's package never mentions EmailSender. It satisfies it just by having the method.
Core Concepts
Implicit Satisfaction
Decoupling is total: an interface can be declared after the types that satisfy it, even in a different module. To assert at compile time that a type satisfies an interface, a common idiom is var _ io.Writer = (*MyWriter)(nil).
Method Sets and Pointer Receivers
If methods are defined on *T (pointer receiver), only *T satisfies the interface, not T. Methods on T (value receiver) are available to both T and *T. A frequent compile error is "T does not implement X (method has pointer receiver)"; pass &t instead.
Small, Composable Interfaces
The standard library's most powerful interfaces have one method:
Larger interfaces are built by embedding: type ReadWriteCloser interface { Reader; Writer; Closer }. Because so many types speak io.Reader and io.Writer, you can chain them freely, for example streaming an HTTP response through gzip into a file with io.Copy.
Type Assertions and Type Switches
Asserting to another interface is how Go discovers optional capabilities. io.Copy checks whether the source implements io.WriterTo to take a faster path.
The Empty Interface: any
interface{}, now spelled any, has no methods, so every type satisfies it. It's necessary for truly heterogeneous data (JSON decoding into map[string]any, fmt.Println's arguments), but it throws away type safety. Since Go 1.18, generics often replace any in container and utility code.
Design Guidelines
- Accept interfaces, return structs. Parameters typed as small interfaces make functions flexible and testable. Returning concrete types lets callers use every method and lets you add methods without breaking anyone.
- Define interfaces at the consumer. The package that needs the behavior declares the minimal interface it needs. Implementation packages export concrete types. This is the Go spin on the interface segregation and dependency inversion ideas in SOLID.
- Don't create interfaces preemptively. Wait until you have a second implementation or a test that needs a fake. A one-implementation interface declared "just in case" adds indirection with no benefit.
- Name single-method interfaces with -er:
Reader,Sender,Validator.
Best Practices
Keep Interfaces Minimal
A function that only reads needs an io.Reader, not an *os.File. Narrow parameter types document intent and widen what callers can pass: files, buffers, network streams, and test fixtures.
Use Fakes Over Mocks
Hand-written fakes that satisfy a small interface are short, readable, and don't break when call order changes. Reach for generated mocks (mockery, gomock) only for large interfaces you don't control.
Check Optional Interfaces Explicitly
When behavior depends on an extra capability (http.Flusher, io.Closer), use the comma-ok assertion and fall back gracefully, rather than asserting without ok and panicking.
Common Mistakes
The Nil Interface Gotcha
An interface value is nil only when both its dynamic type and value are nil. Return a literal nil for "no error", never a typed nil pointer. See Go error handling.
Producer-Side "Header" Interfaces
Export the concrete struct instead, and let each consumer declare the few methods it needs.
Overusing any
A function taking any and type-switching over ten cases is usually a sign you want a proper interface with methods, or generics.
FAQ
How do I know which interfaces a type implements?
Go doesn't record it. Satisfaction is structural and computed by the compiler where needed. Your editor (gopls) can list implementations and implemented interfaces. To guarantee a type satisfies an interface, add a compile-time assertion: var _ Iface = (*T)(nil).
Should I return an interface from a constructor?
Usually not. Return the concrete type so callers get the full API and you can add methods freely. Returning an interface makes sense when the concrete type must stay hidden, as with many implementations behind one factory, or the standard library's errors.New returning error.
What's the difference between interfaces and generics in Go?
Interfaces provide runtime polymorphism: one variable can hold different concrete types, and method calls dispatch dynamically. Generics provide compile-time polymorphism: a function is written once and instantiated per type, with full type safety for containers and algorithms. They complement each other, and generic constraints are themselves written as interfaces.
Is any the same as interface{}?
Yes. any is an alias for interface{}, added in Go 1.18 for readability.
Related Topics
- Go — The language overview
- Go Generics — Type parameters and interface constraints
- Go Error Handling — The
errorinterface in practice - Go Testing — Fakes built on small interfaces
- SOLID Principles — Interface segregation and dependency inversion
- Design Patterns — Strategy and adapter patterns via interfaces