Lesson 01 / 13
The Interface Concept
4 of sixteen bindings are established in a declaration, 4 at a use site, 8 in the value — the course's binding split is produced here. The same type satisfies four interfaces and its source has zero references to any of them; the small-interface principle is not advice here but an observation measured by the count of satisfying types falling as method count rises. Limiting measurement: a compile-time conformance proof moves that same binding into the declaration, and the compiler stays silent when the proof is missing.
Published Updated
In this lesson
- Measures that a type implicitly satisfies an interface and that this satisfaction is written nowhere in the source
- Builds and runs the frame that sorts sixteen bindings into declaration, use site, and value classes, producing the course's binding 4/4/8 split
- Shows that the small-interface principle is not advice but a measurable relationship between method count and the number of types satisfying it
- Runs proof that a compile-time conformance proof moves the same binding into a declaration, and that the compiler stays silent when that proof is missing
Contents
The Go Fundamentals course closed by asking who decides a name’s visibility, a dependency’s version, and the enforcement of a formatting rule — all three answers were the language itself: exporting depends on a letter’s case, version selection on the lowest request written in the module file, formatting rule on the formatter itself. This lesson carries the same question one step further in. When a type satisfies an interface, there is no single line that says so — not in the interface’s definition, not in the type’s own source. The compiler knows this binding; someone reading the source cannot see it. So where is this binding established?
This course asks that question sixteen times — across interfaces, error values, generics — and each time writes the answer into one of three places: is the binding explicitly written in the source (declaration), does the compiler establish it at the point a value is used (use site), or is the binding carried inside the value itself at runtime (value)? This lesson counts these three places for the first time and produces the number the rest of the course will refer back to.
Where a Binding Is Established
An interface defines a method set and does nothing on its own — it carries no field, produces no behavior. A type satisfies an interface if it meets that entire method set with its own methods. Satisfaction being implicit means that no word in the type’s source declares this. A type can even satisfy an interface its own author never heard of; the only condition is that the method signatures match.
Satisfaction itself is never written in the source. But a proof that verifies this satisfaction can be added to the source — and then the binding moves into the declaration. This distinction is the topic of the lesson’s last section. For now, a verbal definition of the three classes is enough: which binding stands explicitly in the source, which is established by the compiler at the point of use, which is carried along with the value at runtime — the measurement makes this countable; we already know this information from the start.
The Frame: An Observer That Classifies a Binding
The measurement runs through a small observer that classifies each binding with a single
question. Three package-level counters (declaration, useSite, value) write every
observation to a class; observe is the single entry point, summarize prints the total.
// binding.go — where is a binding established: in a declaration, at a use site, in the value
package main
import "fmt"
var declaration, useSite, value int
func observe(name, location, evidence string) {
switch location {
case "declaration":
declaration++
case "use":
useSite++
default:
value++
}
fmt.Printf("%-40s %-12s %s\n", name, location, evidence)
}
func summarize() {
fmt.Printf("binding: %d declaration, %d use site, %d value\n", declaration, useSite, value)
}
IN1. observe takes each line as three parts: a name, a class, a piece of evidence. The
evidence column carries the real value backing the claim — which line belongs to which class
is already known ahead of time; the frame does not produce that decision, it only makes it
countable. Keeping the counters at package level means the total stays consistent with the
observations whenever summarize is called.
Sixteen Bindings
This frame asks sixteen bindings in order, inside a single package. The measurement opens with interface satisfaction, continues with embedding and a compile-time proof, then moves through the dynamic type of a value placed in the empty interface, the question a type assertion and a type switch ask of a value, an interface defined on the consumer side, an error value’s implicit satisfaction, a sentinel error’s identity, the context a wrapped error carries, and closes with a generic function’s inferred type parameter, a type constraint’s declaration, and the instantiation of a generic type.
// measurement.go — sixteen bindings, split into three locations
package main
import (
"errors"
"fmt"
)
type Writer interface{ Write(m string) string }
type Closer interface{ Close() error }
// Short, an interface defined on the consumer side: the producer type never mentions it.
type Short interface{ Write(m string) string }
// Embedded combines two contracts in a declaration.
type Embedded interface {
Writer
Closer
}
type File struct{ Name string }
func (f File) Write(m string) string { return f.Name + ":" + m }
func (f File) Close() error { return nil }
// Compile-time conformance proof: the writer put this in a declaration.
var _ Embedded = File{}
type Record struct{ Number int }
func (r *Record) Failure() string { return "record" }
var ErrNotFound = errors.New("not found")
type FieldError struct{ Field string }
func (e *FieldError) Error() string { return "invalid field: " + e.Field }
func search(found bool) error {
if !found {
return fmt.Errorf("search failed: %w", ErrNotFound)
}
return nil
}
func validate() error {
return fmt.Errorf("record could not be validated: %w", &FieldError{Field: "name"})
}
func first[T any](d []T) (T, bool) {
var zero T
if len(d) == 0 {
return zero, false
}
return d[0], true
}
type Numeric interface{ ~int | ~float64 }
func sum[T Numeric](d []T) T {
var t T
for _, e := range d {
t += e
}
return t
}
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Add(o T) { s.items = append(s.items, o) }
func (s *Stack[T]) Type() string {
var zero T
return fmt.Sprintf("%T", zero)
}
func main() {
fmt.Println("-- where is a binding established --")
var w Writer = File{Name: "note"}
observe("a type satisfying an interface", "use", "not written anywhere in the source")
observe("compile-time conformance proof", "declaration", "var _ Embedded = File{}")
observe("interface embedding", "declaration", "Embedded = Writer + Closer")
observe("an interface value's dynamic type", "value", fmt.Sprintf("%T", w))
c, ok := w.(Closer)
observe("a type assertion's answer", "value", fmt.Sprint(ok, " ", c != nil))
var kind string
switch w.(type) {
case File:
kind = "File"
default:
kind = "unknown"
}
observe("the branch a type switch selects", "value", kind)
var _ Short = File{}
observe("an interface defined by the consumer", "use",
"File satisfies four interfaces, its source has zero references to them")
var empty any = w
observe("a value's type in the empty interface", "value", fmt.Sprintf("%T", empty))
var r *Record
var h any = r
observe("an interface carrying a nil pointer", "value", fmt.Sprint("pointer nil=", r == nil, " interface nil=", h == nil))
var err error = &FieldError{Field: "name"}
observe("the error interface being satisfied", "use", fmt.Sprintf("%T", err))
wrapped := search(false)
observe("a sentinel error's identity", "value", fmt.Sprint(errors.Is(wrapped, ErrNotFound)))
observe("the text carried by wrapping", "value", wrapped.Error())
var target *FieldError
observe("extracting a type from a wrapped chain", "value", fmt.Sprint(errors.As(validate(), &target), " ", target.Field))
f, found := first([]string{"a", "b"})
observe("a generic function's type inference", "use", fmt.Sprint(f, " ", found))
observe("a type constraint's declaration", "declaration", fmt.Sprint("Numeric sum=", sum([]int{1, 2, 3})))
var stack Stack[float64]
stack.Add(1.5)
observe("instantiating a generic type", "declaration", "Stack[float64] -> "+stack.Type())
summarize()
}
-- where is a binding established --
a type satisfying an interface use not written anywhere in the source
compile-time conformance proof declaration var _ Embedded = File{}
interface embedding declaration Embedded = Writer + Closer
an interface value's dynamic type value main.File
a type assertion's answer value true true
the branch a type switch selects value File
an interface defined by the consumer use File satisfies four interfaces, its source has zero references to them
a value's type in the empty interface value main.File
an interface carrying a nil pointer value pointer nil=true interface nil=false
the error interface being satisfied use *main.FieldError
a sentinel error's identity value true
the text carried by wrapping value search failed: not found
extracting a type from a wrapped chain value true name
a generic function's type inference use a true
a type constraint's declaration declaration Numeric sum=6
instantiating a generic type declaration Stack[float64] -> float64
binding: 4 declaration, 4 use site, 8 value
Of sixteen bindings, 4 are established in a declaration, 4 at a use site, 8 in the value. This split is binding for the rest of the course; later lessons will print the lines of their own slice and refer back to this table in a single sentence. Eight sitting in the value comes from a Go interface value being a pair — it carries both a concrete type and a value inside it; a type assertion, a type switch, comparing a sentinel error, and extracting a type from a wrapped chain are all four readings of that same pair. The real subject of this lesson is the first two lines and the seventh: interface satisfaction itself and its being defined on the consumer side.
Implicit Satisfaction: Four Interfaces, Zero References
File’s source is three lines: one field, two methods (Write, Close). These two method
lines satisfy four separate interfaces — Writer (wants the Write method), Closer (wants
the Close method), Embedded (wants both, by embedding them), and Short (wants only
Write, a separate definition carrying the same signature as Writer). None of these four
interface names ever appear in File’s own source — not in the type File struct line, not
in the bodies of the Write and Close methods. Zero references in the source, four
satisfactions.
This is the concrete result of implicit satisfaction: a type can satisfy an interface its own
author never even saw, because the only condition for satisfaction is that the method
signatures match. The Short interface was defined after Writer in measurement.go,
and File was written with no knowledge of it; despite this, the line var _ Short = File{}
compiles. The seventh row of the table reduces this to a single number: four interfaces, zero
references. This observation is the first appearance of consumer-side definition, the topic of
the fifth lesson — there the same idea will turn into a design decision.
The Small-Interface Principle: Measurement, Not Advice
As an interface’s method count rises, the number of types satisfying it is expected to fall — wanting more methods means a stricter condition. This is the intuition the Design Principles course’s interface-segregation principle rests on; here it is measured not as advice but as a countable relationship.
// small-interface.go — as an interface's method count grows, does the number of types satisfying it shrink
package main
import "fmt"
type OneMethod interface{ Read() string }
type TwoMethod interface {
Read() string
Write(m string)
}
type ThreeMethod interface {
Read() string
Write(m string)
Close() error
}
type ReadOnly struct{}
func (ReadOnly) Read() string { return "read" }
type ReadWrite struct{}
func (ReadWrite) Read() string { return "read" }
func (ReadWrite) Write(m string) {}
type ReadWriteClose struct{}
func (ReadWriteClose) Read() string { return "read" }
func (ReadWriteClose) Write(m string) {}
func (ReadWriteClose) Close() error { return nil }
type WriteOnly struct{}
func (WriteOnly) Write(m string) {}
func main() {
types := []any{ReadOnly{}, ReadWrite{}, ReadWriteClose{}, WriteOnly{}}
oneCount, twoCount, threeCount := 0, 0, 0
for _, t := range types {
if _, ok := t.(OneMethod); ok {
oneCount++
}
if _, ok := t.(TwoMethod); ok {
twoCount++
}
if _, ok := t.(ThreeMethod); ok {
threeCount++
}
}
fmt.Println("types satisfying the one-method interface :", oneCount)
fmt.Println("types satisfying the two-method interface :", twoCount)
fmt.Println("types satisfying the three-method interface:", threeCount)
}
types satisfying the one-method interface : 3 types satisfying the two-method interface : 2 types satisfying the three-method interface: 1
IN2. Four types were built deliberately: three carry Read with growing method sets, the
fourth (WriteOnly) never carries Read at all and falls outside all three interfaces. Had
this last type not existed, the decline would still show, but the observation “there is also a
type that satisfies none of the interfaces” would be lost — the measurement keeps all four
together for this reason.
Tested against three interfaces, the four types’ count falls to 3, 2, 1: as the method set grows from one method to three, the number of the same four types able to satisfy it shrinks. This binding is the same “use site” class seen in the table — each count is established at the line where a type assertion is called, not in the type’s or the interface’s own definition. A small interface means an interface that wants few methods; an interface that wants few methods means an interface most types satisfy. This is not a design tip, it is the direct result of the count above.
Limiting Measurement: Conformance Can Also Be Written in the Declaration
The first row of the sixteen bindings said “use site,” and its proof was indirect: it showed
that satisfaction is not written in the source, not where it is established. This section
gives the direct proof: it deliberately breaks conformance and shows exactly where the
compiler stops. The tool is the compiles function from the Go Fundamentals course’s
unwritten.go frame — it writes a piece of source into a temporary package, calls the
compiler, and reads only whether it passed.
// compiler.go — tests from inside the program whether a piece of source compiles
package main
import (
"os"
"os/exec"
"path/filepath"
)
func compiles(source string) bool {
dir, err := os.MkdirTemp("", "trial")
if err != nil {
return false
}
defer os.RemoveAll(dir)
os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
These two pieces of source carry the same half-implementation — a type writes only one of a two-method interface’s methods. The difference is a single line: the second carries a conformance proof right after the type.
// conformance.go — the compiler stays silent with no proof, stops with a proof in the declaration
package main
import "fmt"
func main() {
sourceA := `package trial
type FullInterface interface {
First() string
Second() string
}
type HalfImplementation struct{}
func (h HalfImplementation) First() string { return "first" }
`
sourceB := sourceA + `
var _ FullInterface = HalfImplementation{}
`
fmt.Println("no proof, never used at all :", compiles(sourceA))
fmt.Println("with proof, right after the type:", compiles(sourceB))
}
no proof, never used at all : true with proof, right after the type: false
HalfImplementation is identical in both sources — the Second method is missing. In the
first source this type is never used anywhere as FullInterface; the compiler stays silent
and the missing method produces no warning at all. In the second source, the only line added
is a conformance proof placed right after the type’s definition; the same gap now stops
compilation. The binding moved into the declaration in the second source — the proof line
writes into the source not the satisfaction itself, but the verification of that
satisfaction, and when that verification breaks, compilation stops right there.
IN3. This measurement has a blind spot: compiles only reads cmd.Run()’s exit status,
never the error text. Making sure the first source really compiles for no other reason
than the missing method was set up by hand — the source was written to carry no problem beyond
Second. The same discipline holds everywhere in the course a measurement calls the compiler.
A type left without a conformance proof never shows its gap as long as it is never used. This is the real cost of saying “established at the use site”: the binding is only tested the moment it is used, and never tested if it is not. A proof line closes this risk by moving it next to the type’s own definition.
Summary
- A type satisfying an interface is implicit: no word declares this in the source; the only condition is that the method signatures match.
- Of sixteen bindings, 4 are established in a declaration, 4 at a use site, 8 in the value — the course’s binding split was produced in this lesson.
- The
Filetype satisfies four interfaces (Writer,Closer,Embedded,Short) and its own source has zero references to any of the four. - The small-interface principle is a measurement, not advice: as method count climbs from one to two to three, the number of the same four types able to satisfy it falls from three to two to one.
- A compile-time conformance proof (
var _ Interface = Type{}) moves the binding into the declaration; without the proof, a missing implementation never shows itself as long as it is not used.
Next Step
In this lesson an interface value’s dynamic type appeared a few times — main.File, read with
%T — but was never dwelt on. The next lesson grows exactly this line: the empty interface,
wanting no method at all, accepts whatever type is placed in it, but the placed type’s
information is never lost. Where does that information go, and how does %T read it back?
To keep your progress and take notes, Log in
My notes
Log in to take notes.