What an interface is
An interface is a set of method signatures. Any type that has all of those methods satisfies the interface — automatically, without declaring intent. This is called structural (or implicit) typing.
type Stringer interface {
String() string
}
type Money struct{ Cents int }
func (m Money) String() string {
return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100)
}
// Money now satisfies fmt.Stringer with no `implements` keyword.
fmt.Println checks for fmt.Stringer at runtime via a type assertion and
will call String() if present.
Small interfaces win
The Go standard library is full of one- or two-method interfaces, e.g.
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
}
and combinations:
type ReadWriter interface {
Reader
Writer
}
The rule of thumb is: define interfaces in the consuming package, not next to the type that implements them. The consumer knows which methods it needs.
Satisfying an interface
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return d.Name + ": woof" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return c.Name + ": meow" }
animals := []Speaker{Dog{"Rex"}, Cat{"Mia"}}
for _, a := range animals {
fmt.Println(a.Speak())
}
Pointer vs value receivers and interface satisfaction
If a method is defined on *T, then *T satisfies the interface but T
does not:
type Greeter interface { Greet() }
type P struct{ Name string }
func (p *P) Greet() { fmt.Println("hi", p.Name) }
var g Greeter
g = P{"x"} // COMPILE ERROR: P does not satisfy Greeter
g = &P{"x"} // OK
This is one of the most common stumbling blocks. The compiler error message spells it out.
Empty interface and any
interface{} (alias any since Go 1.18) is satisfied by every type:
var x any = 42
x = "hello"
x = []int{1, 2}
You'll see any in JSON decoding (map[string]any), generic-ish containers,
and fmt.Println(args ...any). Prefer concrete types or generics where you
can; any defers type checking to runtime.
Type assertions
var x any = "hello"
s := x.(string) // panics if x is not a string
s, ok := x.(string) // safe form: ok=false on mismatch
Type switches
func describe(x any) string {
switch v := x.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int %d", v)
case string:
return fmt.Sprintf("string %q", v)
case fmt.Stringer:
return "stringer: " + v.String()
default:
return fmt.Sprintf("other %T", v)
}
}
Errors
error is a built-in interface:
type error interface {
Error() string
}
Any type with an Error() string method is an error.
Sentinel errors
var ErrNotFound = errors.New("not found")
Compared with errors.Is(err, ErrNotFound).
Custom error types
type ValidationError struct {
Field, Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Reason)
}
Extracted with errors.As:
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("bad field:", ve.Field)
}
Wrapping with %w
return fmt.Errorf("load config: %w", err)
Composing behaviour with embedded interfaces
type readWriter struct {
io.Reader
io.Writer
}
The struct now satisfies io.Reader, io.Writer, and any combination,
delegating to the embedded values.
Interface design checklist
- Keep interfaces small (1–3 methods).
- Define them in the consumer package, not the producer.
- Don't return interfaces from constructors; return concrete types so callers see the full API.
- Accept interfaces, return structs (a common slogan).
Run the example
go run ./part1-language/examples/07_interfaces