9. Interfaces: implicit satisfaction, any, type switches
9.1 Interfaces are satisfied implicitly
There is no implements keyword. A type satisfies an interface simply by
having the right methods — which means you can define an interface after
the type already exists, even in a different package than the type, and
even define it just for a test:
type Storage interface {
Set(key, value string) error
Get(key string) (string, bool)
Delete(key string) error
}
type MemStorage struct{ data map[string]string }
func (m *MemStorage) Set(k, v string) error { m.data[k] = v; return nil }
func (m *MemStorage) Get(k string) (string, bool) { v, ok := m.data[k]; return v, ok }
func (m *MemStorage) Delete(k string) error { delete(m.data, k); return nil }
var s Storage = &MemStorage{data: map[string]string{}} // compiles: shape matches
9.2 Small interfaces are idiomatic — "accept interfaces, return structs"
The standard library's most reused interfaces are one or two methods:
io.Reader (Read([]byte) (int, error)), io.Writer
(Write([]byte) (int, error)), fmt.Stringer (String() string),
error (Error() string), sort.Interface. Small interfaces mean any
type can satisfy many of them at once, and any function that accepts an
interface argument works with anything that shape — this is how
io.Copy(dst io.Writer, src io.Reader) works identically whether the
source is a file, a network socket, an in-memory buffer, or a gzip
decompressor.
9.3 any and type assertions/switches
any is an alias for interface{} — a value of any type at all, with no
methods guaranteed. Getting the concrete type back out requires an
assertion:
func describe(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return fmt.Sprintf("string: %q", x)
case error:
return fmt.Sprintf("error: %v", x)
case nil:
return "nil"
default:
return fmt.Sprintf("unhandled type %T", x)
}
}
A single (non-switch) assertion has a two-value "comma-ok" form that never panics:
if s, ok := v.(string); ok {
use(s)
}
s := v.(string) // panics if v is not a string — only use when you're certain
9.4 The nil-interface trap
An interface value is nil only if both its type and value are nil.
A nil pointer stored inside an interface is not a nil interface —
this is one of the most common "why doesn't err != nil work" bugs:
type MyError struct{}
func (e *MyError) Error() string { return "boom" }
func doWork() error {
var e *MyError = nil
return e // returns a non-nil error interface wrapping a nil *MyError!
}
err := doWork()
fmt.Println(err == nil) // false! surprising to everyone the first time
The fix: functions that return error should return nil explicitly
when there's no error, never a typed nil pointer.
9.5 Real-world example
Day 6's Storage interface, wrapped by a CachedStorage that adds
metrics, is the canonical Go decorator: CachedStorage embeds a
Storage field, implements the same interface, and adds behavior around
each call — no framework, no annotations, just composition plus
interfaces. This exact shape reappears in Chapter 24 as HTTP middleware
and in Chapter 49 as the WAF middleware chain.