Skip to content
beginner Phase 4 · Structs, Methods, and Interfaces

Interfaces and Implicit Satisfaction

Define method sets that types satisfy automatically without declaring implements.

45m
0 problems
Topic Progress 0%

Replace the Switch With a Question

The billing report renders charges as a table, and every new charge type arrives with a new branch:

func area(c Charge) float64 {
	switch v := c.(type) {
	case UnitCharge:
		return v.Units * v.Rate
	case FlatCharge:
		return v.Amount
	case TieredCharge:
		return tierTotal(v)
	}
	panic("unknown charge type")
}

Every call site that understands charge types needs this switch, so every new charge type edits every switch. The fix is to ask the value instead of interrogating it:

type Discharge interface {
	Area() float64
}

func (u UnitCharge) Area() float64  { return u.Units * u.Rate }
func (f FlatCharge) Area() float64  { return f.Amount }
func (t TieredCharge) Area() float64 { return tierTotal(t) }

func report(cs []Discharge) float64 {
	var total float64
	for _, c := range cs {
		total += c.Area()
	}
	return total
}

An interface is a method set — a list of signatures a type must provide. Note what is absent: UnitCharge never says implements Discharge. Go's satisfaction is implicit and structural: if your type has Area() float64, it satisfies the interface. The compiler checks shapes, not declarations, so types from different packages meet interfaces they have never heard of.

That is exactly how the standard library works. io.Writer is:

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

Any type with a matching Write fits — os.File, bytes.Buffer, an http.ResponseWriter, a gzip compressor — and functions like io.Copy(dst Writer, src Reader) accept them all without importing their packages. The interface is the seam: report above depends on a behavior, not on any charge struct, so adding a fourth charge type now means writing one Area() method and zero new switches.

An interface value is a pair: a concrete type and a value of that type, stored together. Method calls dispatch on the pair — that is all polymorphism is here. It also explains the failure mode you will meet in Type Assertions and Type Switches: extracting the concrete value back out is a separate, explicit step.

The Arithmetic of Method Sets

Interfaces introduce one rule that surprises everyone once, and it comes down to which methods a value of the type can call.

The method set of T contains the methods declared with a value receiver. The method set of *T contains both value-receiver and pointer-receiver methods — a pointer can call anything the value can. Interface satisfaction is checked against the method set of whatever you are assigning:

type Clock interface {
	Now() time.Time
}

type SysClock struct{ zone string }

func (c SysClock) Now() time.Time  { return time.Now() }  // value receiver
func (c *SysClock) Calibrate(d time.Duration) {} // pointer receiver
You assign Method set checked Result
SysClock{} to Clock value methods only works — Now is a value method
&SysClock{} to Clock value + pointer methods works
Calibrate required by an interface, value assigned value methods only compile error — the value cannot answer

Flip the receiver and the assignment breaks:

type Adjuster interface{ Calibrate(time.Duration) }

var a Adjuster = SysClock{}  // does not implement Adjuster
var a Adjuster = &SysClock{} // fine

Why the asymmetry exists: a copy of the value cannot be made to point somewhere to run a mutating method — there is no address to hand out. This is the practical reason the receiver-consistency convention from Methods with Receivers matters: one pointer-receiver method on a type means callers should hold *T, and interfaces will silently require it.

Two corollaries worth having ready. A type with no methods satisfies the empty interface (any), which is why any accepts everything — and why you must narrow it with an assertion before use. And an interface is satisfied by having at least the required methods: extra methods are fine, so a concrete type can be handed to many interfaces, each seeing the slice of behavior it asked for.

Interface satisfaction never widens the other way: you cannot assign an interface value to a concrete type without extracting it (next topic), and one interface variable can hold another interface's value only if the method sets allow it. Keep the arithmetic straight — method set of the thing on the left, required methods of the thing on the right — and every interface compile error becomes predictable.

Whoever Calls Decides the Shape

The most useful convention in Go interface design flips the ownership question: the consumer declares the interface, not the producer.

// package billing — declares only what IT needs

type PriceFetcher interface {
	Fetch(ctx context.Context, sku string) (Money, error)
}

type Service struct {
	prices PriceFetcher
}

func NewService(prices PriceFetcher) Service {
	return Service{prices: prices}
}

func (s Service) Quote(ctx context.Context, sku string) (Money, error) {
	m, err := s.prices.Fetch(ctx, sku)
	if err != nil {
		return Money{}, fmt.Errorf("quote %s: %w", sku, err)
	}
	return m.AddTax(), nil
}

Look at what the declaration does not mention: HTTP, JSON, the pricing provider's 40-method client, retries, rate limits. billing depends on one method, so any type with that method qualifies — a real client in production, a struct with a fixed table in tests:

type fakePrices struct{ quote Money }
func (f fakePrices) Fetch(context.Context, string) (Money, error) {
	return f.quote, nil
}

svc := NewService(fakePrices{quote: Dollars(10)})

No mocking framework, no global, no server — the test constructs the behavior it needs (Mocks and Fakes). Had the provider declared a 10-method interface covering everything its client can do, billing would implement all ten methods as forwarders just to satisfy a contract it does not use.

The guidance the community quotes is "accept interfaces, return structs" — accepting an interface gives callers freedom to substitute; returning a struct gives callers concrete, stable fields without an assertion step. NewService takes an interface and hands back Service.

Where should the interface live? In your package, near the function that uses it, named for the role: PriceFetcher, Store, Notifier. One or two methods is the norm — small interfaces compose and are trivial to fake. When a package exports an interface, it should be because it has a use for one (a driver registry, a callback contract), not because it wants to dictate implementations.

The remaining design question — an interface variable holding a concrete value you need back — is where type assertions come in next.

The Standard Library Is Written in Interfaces

Once you can read a method set, half the standard library becomes legible. Three interfaces do more work than the rest combined.

fmt.Stringer — one method:

type Stringer interface {
	String() string
}

func (d Duration) String() string { return fmt.Sprintf("%v", time.Duration(d)) }

fmt.Println(Duration(90)) // uses String() instead of the raw struct dump

Anything with String() string formats itself when passed to fmt. It is the cheapest way to make your type print well, and String() composes: nested values printing your type call it too.

io.Writer and io.Reader — the data seams: Write([]byte) (int, error) and Read([]byte) (int, error). Functions accept the interface, so io.Copy(file, gzip.NewReader(conn)) moves bytes between types that share no common ancestor. A test that needs captured output does not mock a logger — it passes a bytes.Buffer:

func render(w io.Writer, rows []Row) error {
	for _, r := range rows {
		if _, err := fmt.Fprintf(w, "%s\t%d\n", r.Name, r.Total); err != nil {
			return err
		}
	}
	return nil
}

var buf bytes.Buffer
_ = render(&buf, rows) // production passes a file; the test passes a buffer

sort.Interface — three methods (Len, Less, Swap) that any slice type can carry, letting sort.Sort order it. When your type must be usable by code that does not know it, implement the library's small interface rather than asking the library to know your type.

A final pair of nil subtleties, because they connect back to method dispatch. An interface variable with no dynamic type is nil — calling any method on it panics, since there is no function to invoke. And a typed nil (a nil pointer stored in the interface) is not nil — the pair is (*PathError, nil), which is why returning var pe *PathError; return pe as an error defeats the caller's if err != nil check (Methods with Receivers). The interface holds a type even when its value is nil; only an interface declared with no type at all compares equal to nil. Method sets make types satisfy interfaces; assertions are how you see, and safely unwrap, what an interface actually holds.

Quiz

1. `Clock` requires `Now()`. `SysClock` declares only `func (c *SysClock) Now() time.Time`. Which assignments compile?

Question 1 options

2. A service needs to fetch prices from a third-party SDK client with 14 methods. Where should its interface be declared?

Question 2 options

3. Why does `io.Copy(dst, src)` work with files, buffers, and network connections alike?

Question 3 options

4. A function returns `var pe *PathError; return pe` with signature error, and the caller checks `if err != nil`. What happens?

Question 4 options

Flashcards

Question

How does a type satisfy an interface in Go, and what keyword do you never write?

Answer

Implicitly and structurally: if the type's method set includes every method of the interface, it satisfies it — no implements declaration exists. Types and interfaces in different packages still match, as long as the signatures line up.

Question

What is the method set of T versus *T, and why does it decide interface assignment?

Answer

T's method set holds only value-receiver methods; *T's holds both. An interface assignment is checked against the method set of what you assign, so a type with a pointer-receiver method can only be used as *T — a value copy has no address to run the mutating method with.

Question

Who should declare an interface — the package that implements the behavior or the package that uses it?

Answer

The consumer, near the using function, named for the role (PriceFetcher, Store). It keeps the contract one or two methods wide, lets tests swap in a hand-written fake, and stops your service from depending on a provider client's entire method surface.

Question

What does "accept interfaces, return structs" buy you?

Answer

Accepting an interface lets callers inject any implementation — real client in production, fake in tests. Returning a struct gives callers concrete, documented fields without a type assertion, and avoids making them depend on an interface just to read data.

Question

Why are most good Go interfaces one to three methods long?

Answer

Small method sets are easy to satisfy, easy to fake in tests, and compose by embedding (Reader + Closer). io.Writer works because Write is the whole behavior; interfaces grow to useful size only when they capture a genuine protocol, like sort.Interface's Len/Less/Swap.

Revision Notes

Key Takeaways

  • 1. Interfaces are method sets satisfied implicitly — no implements keyword, no shared base type
  • 2. The method set of T excludes pointer-receiver methods; *T includes them, so pointer-receiver types must be assigned as pointers
  • 3. Consumers declare the interfaces they need, one or two methods wide, so tests can swap in fakes without frameworks
  • 4. An interface value is a (type, value) pair: typed nils defeat nil checks, and unwrapping requires assertions
  • 5. Small stdlib interfaces — Stringer, Writer, Reader — are the composition points of the whole standard library

Interview Tips

  • • "What is an interface in Go?" — a method set satisfied structurally; contrast with Java/C# explicitly, then give the io.Writer example
  • • "Why does this not compile: `var c Clock = SysClock{}`?" — walk the method set table: pointer-receiver methods are invisible to the value's method set
  • • "Where would you declare the interface for your payment client?" — in the consuming package, one or two methods, role-named; explain the test-fake benefit
  • • "Explain nil vs typed nil" — the (type, value) pair model; use the `var pe *PathError; return pe` error bug as your worked example

Cheat Sheet

Interfaces and Implicit Satisfaction

Satisfaction: structural, not declared — type qualifies iff its method set contains every interface method. Extra methods are harmless.

Method sets:

Assigning Checked method set Pointer-receiver methods count?
T{} value methods only no
&T{} value + pointer methods yes

Design rules: declare interfaces on the consumer side, role-named, 1-2 methods; accept interfaces, return structs; fake in tests with a tiny hand-written type.

Stdlib method sets to know: fmt.Stringer{String() string}, io.Writer{Write([]byte) (int, error)}, io.Reader{Read([]byte) (int, error)}, sort.Interface{Len/Less/Swap}.

Nil rules: interface == nil only if type and value are both absent; typed nil (nil *T in an interface) is not nil; calling a method on a nil interface panics.

Code template:

type Store interface {
	Get(ctx context.Context, id string) (Row, error)
}

type service struct{ db Store }

func NewService(db Store) *service { return &service{db: db} }

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading