Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36