Explain "accept interfaces, return structs". When is returning an interface justified?
Question 95MediumGo 1.22 to 1.25
Accept interfaces: parameters should ask for the smallest behaviour needed (io.Reader rather than *os.File). Callers can then pass files, buffers, network connections or test fakes.
Return structs: constructors should return the concrete type (*Client). Callers get the full API, can reach fields and extra methods, can define their own narrower interfaces, and you can add methods later without breaking anyone. Adding a method to an exported interface is a breaking change for everyone who implements it.
// Good
func NewParser(r io.Reader) *Parser { return &Parser{r: bufio.NewReader(r)} }
// Usually bad: hides the concrete type, forces the interface on every caller,
// and invites the typed-nil bug
func NewParserBad(r io.Reader) ParserInterface { ... }
Returning an interface is justified when:
- The function can return several concrete types:
errors.Newreturnserror,io.MultiReaderreturnsio.Reader,crypto/sha256.Newreturnshash.Hash. - The concrete type is deliberately unexported, so the implementation can change freely (
context.Context). - The result is a factory or plugin chosen at runtime.
The guideline is a default, not a law. Interviewers want to hear the reasoning (API evolution, caller freedom, avoiding typed nils) rather than the slogan.
More on Interfaces, Methods & Embedding
- Q93What does
var _ io.Writer = (*MyWriter)(nil)do, and why is it used? - Q94Go interfaces are satisfied implicitly. What design consequences follow, and where should interfaces be defined?
- Q96Why does Go favour small interfaces? Give examples of composition.
- Q97What's wrong with this
String()method? When doesfmtcallString()orError()? - Q98Design custom errors in Go. Explain wrapping,
errors.Is,errors.Asand multi-error trees. - Q99When does comparing two interface values panic? What does this print?