Interfaces deep, method sets, and composition over inheritance
Go's interfaces are the most underrated feature in the language. They are structurally typed (no implements keyword), satisfied at compile time, and the entire standard library is built on small, single-method ones (io.Reader, io.Writer, error, fmt.Stringer). Senior Go interviews live in this territory.
This lesson is the deep view: method sets, the value-vs-pointer receiver rule everyone gets wrong on first try, how to size interfaces, and why "no inheritance" is a feature.
Interfaces are structural, satisfied implicitly
There is no implements keyword in Go. A type satisfies an interface if it has all the methods. That is the entire test.
type Stringer interface {
String() string
}
type User struct{ Name string }
func (u User) String() string { return u.Name }
// User now satisfies Stringer. Nobody had to declare it.This is huge in practice. You can write an interface in package A and have types from package B satisfy it without B ever importing A. This is how the standard library composes so cleanly.
Method sets: the rule that trips everyone
The rule, stated carefully: the method set of a type T contains all methods declared with a value receiver (t T). The method set of *T contains both value-receiver methods AND pointer-receiver methods (t *T).
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // value receiver
func (c *Counter) Inc() { c.n++ } // pointer receiverCounterhasValue()only.*Counterhas bothValue()andInc().
If an interface requires Inc(), only *Counter satisfies it, never Counter.
Interview trap: "Why does this not compile?" The code is something like passing a
Counter{}to a function taking an interface that requiresInc(). The answer:Incis on*Counter, notCounter, so the value does not satisfy the interface. Pass&Counter{}instead.
The discipline rule: pick one receiver style per type. If any method needs a pointer receiver (because it mutates or because the struct is large), make all methods pointer receivers. Mixing is the most common source of "does not satisfy interface" errors in real codebases.
Accept interfaces, return structs
The single most repeated piece of senior Go advice. Functions take interface parameters (small, specific) and return concrete types (structs).
// good: caller can pass anything that can be read from
func Load(r io.Reader) (*Config, error) { ... }
// less good: caller must hand you a real *os.File
func Load(f *os.File) (*Config, error) { ... }Returning a concrete type means the caller sees the full method set, can rely on specific behaviour, and can later wrap it themselves if they need an interface. Returning an interface throws information away and makes mocking harder, not easier.
Define interfaces where you USE them
This is the Go ethos and it is genuinely different from Java or C#.
In Java, the producer of a type defines the interface and the consumer imports it. In Go, the consumer defines the interface they need, and any type from any package can satisfy it. The interface lives next to the function that takes it.
// in package payments, where you USE the interface
type ledger interface {
Credit(amt int64) error
}
func Settle(l ledger, amt int64) error { return l.Credit(amt) }The ledger interface is one method, lowercase, unexported, defined exactly where it is consumed. The Settle function does not care which package the concrete ledger comes from.
Senior rule: define interfaces where you USE them, not where you implement them. The consumer owns the interface, not the producer.
Concretely: do not define type UserService interface { ... } in your userservice package and then implement it in the same package. Define the interface in the package that needs it (the HTTP handler, the worker, the test), with only the methods that consumer needs.
Interface composition
Interfaces can embed other interfaces. The standard library does this constantly.
type Reader interface { Read(p []byte) (int, error) }
type Closer interface { Close() error }
type ReadCloser interface {
Reader
Closer
}io.ReadCloser is exactly that. Any type with both Read and Close satisfies it. This is how http.Response.Body is typed: a ReadCloser because you read it and then close it.
Compose small interfaces into bigger ones. Never define a 12-method "service" interface that callers only use 2 methods from. Split it.
The empty interface (any) trap
interface{} (now spelled any) is the interface with zero methods. Every type satisfies it. It is also the loophole that turns your code into dynamically-typed JavaScript.
func process(x any) {
s, ok := x.(string) // type assertion, runtime check
if !ok { return }
fmt.Println(s)
}You lose compile-time type safety. You pay an allocation (interface boxing puts the value on the heap). You make the caller's life worse because they have to read the function body to know what types are accepted.
Use any for genuinely heterogeneous data (JSON unmarshalling into unknown shape, logging structured fields, middleware pipelines). Otherwise prefer generics (next lesson) or a concrete interface.
Type assertions and type switches
When you have an interface value and need the underlying type.
switch v := err.(type) {
case *net.OpError:
log.Println("network op:", v.Op)
case *url.Error:
log.Println("url:", v.URL)
default:
log.Println("other:", err)
}This is the idiomatic pattern in error handling, request routers, and anywhere you take any but expect a small set of real types. Always have a default case, even if it is just to log "unexpected type".
No inheritance, only composition
Go has no class hierarchy. It has struct embedding, which is composition with a syntactic sugar that promotes the embedded type's methods to the outer struct.
type Logger struct{}
func (Logger) Log(msg string) { fmt.Println(msg) }
type Server struct {
Logger // embedded
Addr string
}
// s.Log("ok") works; the method is promotedThis is not inheritance. There is no "Server is a Logger" relationship. There is no polymorphism through the embedded type. It is just method forwarding. Treat it as a convenience, not as a design pattern import from Java.
Interface concepts
- method set
- The set of methods callable on a type. *T has all methods of T plus pointer-receiver methods.
- structural typing
- Interface satisfaction by shape, not by declaration. No 'implements' keyword in Go.
- interface composition
- Embedding interfaces inside larger interfaces. io.ReadCloser is Reader + Closer.
- type assertion
- Runtime check that extracts the concrete type from an interface value: v, ok := x.(T).
- empty interface
- interface{} or any. Satisfied by every type. Loses compile-time type safety; use sparingly.
- struct embedding
- Including a type by name in a struct. Promotes its fields and methods. Composition, not inheritance.
Senior rule: define interfaces where you USE them, not where you implement them. The consumer owns the interface, not the producer.
Common questions
Q.Should I define interfaces in the package that implements them or the package that uses them?›
Q.What is the difference between value and pointer receivers for satisfying an interface?›
Watching quietly. Tap me if you want a tip.
Go cannot run natively in a browser. Run copies your code and opens go.dev/play ; paste and click Run there.
Try this (0 of 1 done)
- 1
Predict both outputs (each on its own line).
hint
Pointer receiver requires &Cat{...} to satisfy the interface.
show answer
// already in the editor