skip to content

Interface Design and Embedding

Idiomatic Go keeps interfaces tiny and defines them where they are used, then composes bigger ones by embedding. This is the design half of a Go interview: why 'accept interfaces, return structs' is advice rather than dogma, and why a mirror-image interface per struct adds nothing.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does Go's io.ReadWriteCloser gain by embedding io.Reader, io.Writer and io.Closer?

level: juniorimportance: must knowfreq 64%

answer

  1. three small names, one new interface
  2. no new method signatures are written
  3. the method set is a union
  4. no code is inherited, only requirements
  5. any type with all three qualifies

basics

~20 s

Embedding folds the other interfaces' method sets into the new one, so io.ReadWriteCloser requires exactly Read, Write and Close and declares nothing extra. Any type with all three methods satisfies it automatically, with no declaration of intent.

solid answer

~50 s

An interface body may list embedded interface names instead of method signatures, and the resulting method set is the union of them. `io.ReadWriteCloser` is declared as `interface { Reader; Writer; Closer }`, so it means "has `Read`, `Write` and `Close`" and adds nothing of its own. Satisfaction stays implicit: `*os.File` never mentions the interface, it just has the three methods. Because the method set is a superset, a value of type `io.ReadWriteCloser` can be passed anywhere an `io.Reader` is wanted. Embedding here is pure composition of requirements, not inheritance -- an interface holds no code, so nothing is inherited and there is no default implementation. It is the reason the standard library keeps interfaces at one method each: the small ones are the vocabulary, and the compound ones are spelled out of them where needed.

code

go · 18 lines
go
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

// ReadWriteCloser declares no method of its own.
type ReadWriteCloser interface {
	Reader
	Writer
	Closer
}

go deeper

for a junior

Be ready to say that io.ReadWriteCloser is just Read plus Write plus Close, and that a type satisfies it simply by having those three methods with matching signatures.

for a middle

An interviewer expects the mechanics: embedding unions method sets at compile time, adds no code, and makes the compound interface assignable to any of its parts but not the other way round.

for a senior

Show the design consequence in review: prefer composing small interfaces at the call site over declaring one wide interface, so callers and test fakes only carry the methods that are actually used.

for a principal

Own the vocabulary decision for a shared package. Small embeddable interfaces are cheap to extend by composition, whereas a wide compound interface exported early fixes an obligation on every downstream implementer.

## What an interface declaration may contain A Go interface type lists what a value must be able to do. Its body can hold two kinds of line: a **method signature**, and the **name of another interface**, which is called *embedding*. When you embed an interface, every method it requires becomes a requirement of the new interface too. The resulting method set is the union of the embedded sets plus any methods you spelled out directly. The `io` package is the canonical demonstration: - `io.Reader` requires `Read(p []byte) (n int, err error)` - `io.Writer` requires `Write(p []byte) (n int, err error)` - `io.Closer` requires `Close() error` - `io.ReadWriteCloser` embeds all three and declares nothing else So `io.ReadWriteCloser` is not a bigger idea than its parts; it is a *name* for the conjunction of them. `io.ReadWriter`, `io.ReadCloser` and `io.WriteCloser` exist in the same family for the other combinations. ## Satisfaction stays implicit Go has no `implements` keyword. A type satisfies an interface when its method set contains every required method with an identical signature -- the compiler checks that at the point of assignment or call, not at the point of declaration. `*os.File` has `Read`, `Write` and `Close` (among many others), so it satisfies `io.ReadWriteCloser` without the `os` package having heard of the interface. Your own type does exactly the same: write the three methods and you are done. A common beginner mistake is thinking the implementing struct must itself embed `io.Reader` -- it must not; embedding an interface into a *struct* is a different mechanism with different consequences. ## Embedding is not inheritance An interface contains no code at all -- only requirements. So embedding cannot hand an implementer a default `Read`, cannot provide a base implementation, and has no notion of overriding or of calling "up". Everything embedding does is arithmetic on method sets at compile time. This is why the compound interfaces cost nothing at run time: an interface value is still a pair of (dynamic type, value), and calling `Close` through `io.ReadWriteCloser` costs the same as calling it through `io.Closer`. ## Assignability runs from wide to narrow Because satisfaction is by method set, a value typed as `io.ReadWriteCloser` may be assigned to a variable of type `io.Reader`, or passed to `func Count(r io.Reader)`. The wider interface's method set is a superset of the narrower one's, so the requirement is met. The reverse does not work: an `io.Reader` cannot be assigned to an `io.ReadWriteCloser`, because the compiler has no evidence that `Write` and `Close` exist. Getting from narrow back to wide requires a run-time type assertion, which is a different topic. ## Duplicate methods across embedded interfaces Embedding two interfaces that both require a method with the same name is fine in current Go as long as the signatures are identical -- the union simply contains it once. If the signatures differ, the declaration is a compile-time error, because no single type could satisfy both. ## Why this shape matters for design The payoff is that the small interfaces are reusable and the compound ones are free. If `io` had started with one large `File` interface, every function would have demanded far more than it used, and every fake in a test would have had to implement the lot. Instead each function asks for the narrowest thing it actually calls: `io.Copy` takes an `io.Writer` and an `io.Reader`, so it works over files, network connections, `bytes.Buffer`, HTTP bodies and pipes alike, none of which were designed with `io.Copy` in mind. When you design your own interfaces, follow the same order: write the one- and two-method interfaces that describe single capabilities, and compose them by embedding only where a caller genuinely needs the combination. A three-line interface that embeds two others reads better than a six-method one, and each half stays independently usable. ## What to remember Embedding an interface inside an interface unions method sets, adds no implementation, is resolved entirely at compile time, and keeps the standard library's small interfaces composable.

  • Can a value typed io.ReadWriteCloser be passed to a function that takes io.Reader?
    Yes. Assignability between interfaces is decided by method sets, and `io.ReadWriteCloser`'s set is a superset of `io.Reader`'s, so the compiler accepts it. The reverse fails: an `io.Reader` has no proven `Write` or `Close`, so widening needs a run-time type assertion instead.
  • What happens if you embed two interfaces that both declare a method with the same name?
    Current Go accepts it as long as both signatures are identical -- the method appears once in the union. If the signatures differ, the interface declaration itself is a compile-time error, since no type could ever satisfy both requirements at once.
  • Does embedding io.Reader give implementers a Read method for free?
    No. An interface holds requirements, never code, so embedding can only add obligations. Anything satisfying the compound interface must supply all three method bodies itself. That is the sharp difference from embedding a concrete type in a struct, where real methods are promoted.

It is like a job advert that says "must hold the three certificates listed below" rather than teaching you anything: it states requirements, it does not supply skills.

saying these in an interview costs you the question

  • Says embedding gives implementers a default Read implementation
  • Thinks the implementing struct must also embed io.Reader
  • Believes a type must declare that it implements io.ReadWriteCloser
  • Claims io.ReadWriteCloser adds methods beyond Read, Write and Close
  • Calls interface embedding inheritance from a base interface
open as a page

Why does Go idiom say to accept interfaces and return concrete types?

level: middleimportance: must knowfreq 72%

basics

~20 s

An 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.

open as a page

An exported 12-method interface in your Go proxy package is used by every downstream service. How do you shrink it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Measure which methods each consumer actually calls, then add small one- and two-method interfaces beside the big one and move parameters onto them. Redeclare the wide interface as an embedding of those, so existing code still compiles, then delete it.

open as a page

As the author of a shared Go library, how do you decide whether to export an interface at all, and how wide?

level: principalimportance: should knowfreq 33%

basics

~20 s

Default to exporting concrete types; export an interface only where several real implementations must exist or your own API takes it as a parameter. Size it by what callers call, since an exported interface can never change without breaking implementers.

open as a page

Why does net/http declare Handler as a one-method interface and also provide HandlerFunc?

level: middleimportance: nice to knowfreq 45%

basics

~20 s

Because http.Handler requires just ServeHTTP, a plain function can implement it: http.HandlerFunc is a named function type with a ServeHTTP method that calls itself. Keeping an interface at one method is what makes such a function adapter possible.

open as a page