Why does Go idiom say to accept interfaces and return concrete types?
answer
- parameters state needs, results state guarantees
- an interface result can only subtract
- who picks the abstraction, you or the caller
- adding a method to a struct breaks nobody
- error is the famous exception
basics
~20 sAn interface parameter lets a function work with any implementation, including a stub in a test. Returning the concrete type keeps every field and method reachable for the caller, who can then narrow to whatever interface the calling code actually needs.
solid answer
~40 sThe asymmetry follows from where the abstraction is useful. On input, a function should ask for the smallest capability it calls -- `io.Writer` rather than `*os.File` -- so any implementation fits and callers are not forced to hand over more than the function uses. On output, an interface only subtracts: return `io.Reader` from a constructor and the caller loses `Close`, `Seek` and every method you add later, and you have picked their abstraction for them. Returning `*Proxy` costs the caller nothing, because they can always assign it to a narrower interface at their own boundary. Returning an interface also makes it possible to hand back a non-nil interface wrapping a nil pointer. The guideline has real exceptions: `error` is an interface everywhere, and driver-style seams with several implementations genuinely return one.
code
go · 13 linestype Proxy struct {
next http.Handler
}
// Accepts an interface: any handler fits, including a test stub.
func NewProxy(next http.Handler) *Proxy {
return &Proxy{next: next}
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.Header.Set("X-Forwarded-Proto", "https")
p.next.ServeHTTP(w, r)
}go deeper
Remember the phrase and one example: take io.Writer as a parameter so any destination works, and return the concrete struct so the caller keeps all its methods.
Explain both halves mechanically: a parameter should demand only the methods the body calls, while an interface result hides methods, freezes the surface and picks the caller's abstraction for them.
Use it as a review rule. Catch parameters wider than the code uses, spot constructors returning interfaces with a single implementation, and name error and driver seams as the legitimate exceptions.
Own the cost asymmetry across teams. Adding a method to an exported struct is free, adding one to an exported interface is a breaking change for everyone implementing it, so decide deliberately which of those bills your package is willing to send.
## The guideline "Accept interfaces, return concrete types" is one of the most quoted pieces of Go advice, and the reason for its asymmetry is worth understanding rather than memorising. ## Why accepting an interface helps A function parameter states a requirement. If a middleware constructor takes `http.Handler`, then anything with `ServeHTTP` can be passed: the real router, another middleware, a one-line stub in a test. If it took a concrete `*Router` instead, every caller would have to produce that exact type, and testing the middleware in isolation would mean constructing a router. The strength of the parameter should be the *narrowest* capability the body actually uses. A function that only writes bytes should take `io.Writer`, not `io.ReadWriteCloser` and certainly not `*os.File`. Every extra method in the parameter type is an obligation you impose on every caller and every fake, for nothing. This is the practical form of interface segregation in a language with no implementation inheritance: keep the parameter to one or two methods. ## Why returning a concrete type helps A return type states a *guarantee*, and wrapping it in an interface can only remove guarantees. 1. **You throw away methods.** `func OpenLog(name string) (io.Reader, error)` hands the caller an open file they cannot `Close` or `Seek` without a type assertion. The `*os.File` they really have is strictly more useful. 2. **You freeze the surface.** Adding a method to your concrete type is a backwards-compatible change nobody has to react to. If your API instead returns an interface you export, adding a method to that interface breaks every outside implementer. Returning the struct keeps evolution cheap. 3. **You choose the caller's abstraction.** Different callers need different slices of your type. If you return the concrete type, each of them can accept exactly the shape their own code needs. If you return an interface, you have decided in advance and everyone lives with it. 4. **You open the typed-nil hazard.** An interface value is a (type, value) pair, so returning a nil `*MyClient` as a non-nil interface produces a value that is not `== nil` but panics on use. Concrete return types cannot get into that state. ## Where the guideline does not apply It is a default, not a law. - **`error` is an interface** and is returned by nearly every function in Go. That is deliberate: the caller almost always only tests it, wraps it or prints it, and the set of concrete error types is open by design. - **Genuine plugin seams** return interfaces. A driver registry that picks an implementation at run time from configuration has no single concrete type to return, and `database/sql/driver` and `hash.Hash` exist for precisely that reason. - **Unexported concrete types.** If the implementation type is deliberately hidden, returning a small exported interface is the honest option -- but keep it small, because you now own it forever. - **Several implementations already exist.** If the function must choose between two real types at run time, an interface (or a small wrapper struct) is the only way to express it. ## A worked shape A proxy package might export `func NewProxy(next http.Handler) *Proxy`. The parameter is an interface, so the proxy can be stacked in front of anything, tested against a two-line handler, or fronted by another middleware. The result is `*Proxy`, so a caller can reach its `SetTimeout`, its metrics counters, or a method added in a later release, and can still store it in an `http.Handler` variable when it wants to. Compare that with `func NewProxy(next http.Handler) http.Handler`. Convenient at first, but now nothing about the proxy is reachable, the package cannot grow the type usefully, and callers who need one extra method are forced into a type assertion against an unexported type. ## Reviewing for it On a pull request, two questions catch most violations. First: does this parameter use every method of the type it demands? If not, narrow it. Second: is this return type an interface, and if so, who else implements it? If the answer is "only us", return the struct. ## What to remember Inputs should be as weak as the function's needs; outputs should be as strong as the truth allows. Interfaces on the way in buy flexibility for the caller; interfaces on the way out spend it.
- Go returns error, an interface, from nearly every function. Does that contradict the guideline?No -- it marks the exception. `error` exists because the set of concrete error types is deliberately open and callers almost never need the concrete type, only to test, wrap or print it. The guideline targets constructors returning your own single implementation, where an interface removes methods for no gain.
- When is returning an interface the right call?When more than one real implementation is selected at run time -- a driver or backend chosen from configuration -- or when the concrete type is deliberately unexported. In both cases keep the returned interface to one or two methods, because you now own its shape for as long as the package is public.
- How does returning a concrete type make evolution cheaper?Adding a method to an exported struct is backwards compatible: nobody implements a struct, so nothing outside can break. Adding a method to an exported interface breaks every outside implementer at compile time, which in a versioned module means a new major version.
saying these in an interview costs you the question
- Returns an interface from every constructor on principle
- Says returning an interface is more flexible for the caller
- Declares a wide interface mirroring one struct's methods
- Takes a concrete type as a parameter and calls one method on it
- Cannot name error as the standard exception