The best way to internalize a pattern is to spot it in code you already use. Go’s standard library is full of them — often in a form so natural you never noticed there was a “pattern” at all. Here’s a map.
🎯 Why this matters
If you can see that gzip.NewWriter is a Decorator and sort.Slice is a Strategy, you’ve understood both the pattern and idiomatic Go at the same time. Patterns stop being abstract and become muscle memory.
The map
| Pattern | In the standard library | How it shows up |
|---|---|---|
| Decorator | gzip.NewWriter, bufio.NewReader, io.MultiWriter | Each wraps an io.Writer/io.Reader and adds behavior while keeping the same interface. |
| Adapter | strings.NewReader, bytes.NewBuffer, http.HandlerFunc | Make one type satisfy the interface another API expects. |
| Strategy | sort.Slice(s, less), strings.Map, template.FuncMap | Pass the algorithm in as a function. |
| Iterator | bufio.Scanner, sql.Rows, range-over-func (Go 1.23+) | Walk a sequence without exposing its internals. |
| Singleton | sync.Once, http.DefaultClient, package init() | Exactly one instance, initialized once. |
| Factory | sql.Open, crypto/tls config → tls.Conn | A function returns an interface; the concrete type is chosen for you. |
| Template Method | http middleware, text/template execution | A fixed skeleton with caller-supplied steps. |
| Chain of Responsibility | net/http middleware chains | Each handler can act, then pass to the next. |
| Observer / Pub-Sub | channels + goroutines, context.Context cancellation | Broadcast a change to many listeners. |
| Command | os/exec.Cmd, flag actions | An action captured as a configurable object. |
| Visitor | filepath.WalkDir, go/ast.Inspect | An operation applied across a structure’s nodes. |
| Null Object | io.Discard | A do-nothing implementation that removes nil checks. |
| Bridge | io.Writer ↔ os.File, net.Conn | The interface separates the consumer from concrete implementations. |
A few worth seeing up close
Decorator — io wrappers
// Each layer wraps the previous and keeps the io.Writer interface.
f, _ := os.Create("out.gz")
gz := gzip.NewWriter(f) // decorate: add compression
buf := bufio.NewWriter(gz) // decorate: add buffering
fmt.Fprintln(buf, "hello") // writes flow buf → gz → file
Strategy — sort.Slice
people := []Person{{"Ada", 36}, {"Linus", 54}}
sort.Slice(people, func(i, j int) bool { // the comparison strategy
return people[i].Age < people[j].Age
})
Adapter — http.HandlerFunc
// A plain function becomes an http.Handler by adapting it.
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hi")
})
Spot five patterns in one program
This short program quietly uses five patterns from the table — all standard library, no naming required. Edit and Run:
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"sort"
"strings"
)
func main() {
// Decorator + Null Object: MultiWriter fans one write to many sinks;
// io.Discard is a do-nothing writer (no nil checks needed).
var buf bytes.Buffer
w := io.MultiWriter(&buf, io.Discard)
fmt.Fprint(w, "banana apple cherry date")
// Adapter: strings.NewReader makes a string satisfy io.Reader.
// Iterator: bufio.Scanner walks tokens without exposing the buffer.
sc := bufio.NewScanner(strings.NewReader(buf.String()))
sc.Split(bufio.ScanWords)
var words []string
for sc.Scan() {
words = append(words, sc.Text())
}
// Strategy: the comparison algorithm is passed in as a function.
sort.Slice(words, func(i, j int) bool { return words[i] < words[j] })
fmt.Println("sorted words:", words)
}
Five patterns — Decorator, Null Object, Adapter, Iterator, Strategy — and not one of them needed a class, an implements, or even a comment to exist.
🐹 The takeaway
Go rarely names these patterns, because its interfaces and functions make them feel like ordinary code. That’s the point: in Go, a “pattern” is usually just the natural shape the language nudges you toward.
As you work through the individual pattern pages, come back here — each one links to its standard-library home so you can connect the theory to code you ship every day.
Related patterns
Attach new behavior to an object by wrapping it in another object of the same interface — without changing the original's type.
behavioralStrategyDefine a family of interchangeable algorithms, encapsulate each one, and select which to use at runtime.
structuralAdapterConvert the interface of a type into another interface clients expect, letting otherwise-incompatible types work together.
behavioralIteratorProvide a way to access the elements of a collection sequentially without exposing its underlying representation.
Check your understanding
Score: 0 / 51. gzip.NewWriter(w) wraps an io.Writer and returns an io.Writer that compresses. Which pattern?
It keeps the io.Writer contract and adds compression — a Decorator. bufio.NewWriter and io.MultiWriter are the same idea; chain them to stack behaviors.
2. sort.Slice(s, less) takes the comparison as a function. Which pattern is that?
The sort is fixed; the comparison strategy is supplied as a func. strings.Map and template.FuncMap inject strategies the same way.
3. io.Discard is a writer that throws everything away. Which pattern does it embody?
Instead of checking `if w != nil` everywhere, hand code a Null Object (io.Discard) that satisfies the interface and does nothing — simpler, branch-free call sites.
4. http.HandlerFunc turns a plain func(w, r) into an http.Handler. Which pattern?
HandlerFunc gives a function a ServeHTTP method so it fits the http.Handler interface — a function adapter. sort.Reverse adapts a sort.Interface similarly.
5. Why do these patterns 'feel invisible' in Go?
Where Java needs classes and explicit wiring to express a Strategy or Adapter, Go expresses them as a func value or a type that just has the right methods — so the pattern dissolves into idiomatic code.
Comments
Sign in with GitHub to join the discussion.