# GoF Patterns in Go

23 runnable minimal implementations: 5 creational, 7 structural, 11 behavioral. Go 1.23+, standard library only.

These examples teach pattern invariants, not production frameworks. Mutable examples are single-owner except for the documented Singleton/Flyweight/Observer concurrency support. Callers provide valid non-nil interface dependencies.

## 01 · Factory Method — creational

Delegate product creation to a replaceable creator.

### Problem

The delivery workflow is stable, but deployments need email or SMS. Constructing a concrete sender inside the workflow hard-codes that choice.

### Design

Deliver knows only Creator and Sender. It asks NewSender for a product, then calls Send. Replacing the creator changes the product without editing the workflow. GoF uses subclass overriding; this version expresses the same variation point through interfaces and composition.

### Walkthrough

Read Creator.NewSender, then the two calls in Deliver, then the concrete creators. The table test passes both through the same caller contract.

### Boundary

A function named NewX is not automatically Factory Method. The replaceable creation operation matters. With one known concrete type, a plain constructor is simpler.

### Role map

| GoF role | Go implementation |
|---|---|
| Creator | `Creator` |
| Factory operation | `NewSender` |
| Product | `Sender` |
| Concrete products | `email / sms` |

### Minimal Go

```go
package factorymethod

type Sender interface{ Send(string) string }
type Creator interface{ NewSender() Sender }

// Deliver depends on a creation operation, not a concrete product.
func Deliver(c Creator, message string) string { return c.NewSender().Send(message) }

type EmailCreator struct{}

func (EmailCreator) NewSender() Sender { return email{} }

type SMSCreator struct{}

func (SMSCreator) NewSender() Sender { return sms{} }

type email struct{}

func (email) Send(s string) string { return "email:" + s }

type sms struct{}

func (sms) Send(s string) string { return "sms:" + s }
```

### Test

```go
package factorymethod

import "testing"

func TestProductsAreSubstitutable(t *testing.T) {
	for _, tc := range []struct {
		creator Creator
		want    string
	}{{EmailCreator{}, "email:hi"}, {SMSCreator{}, "sms:hi"}} {
		if got := Deliver(tc.creator, "hi"); got != tc.want {
			t.Fatal(got)
		}
	}
}
```

```sh
go test -v ./factory-method
```

### OSS · Go adaptation

gRPC balancer.Builder.Build returns Balancer; baseBuilder.Build creates baseBalancer. This is a creation extension point. Despite its name, this Builder plays a Factory Method role rather than performing step-by-step assembly.

- [grpc/grpc-go / balancer/balancer.go: L221–L228](https://github.com/grpc/grpc-go/blob/298389d61dd6f3071da1fa34d2fdf3f726d4aa36/balancer/balancer.go#L221-L228)
- [grpc/grpc-go / balancer/base/balancer.go: L35–L61](https://github.com/grpc/grpc-go/blob/298389d61dd6f3071da1fa34d2fdf3f726d4aa36/balancer/base/balancer.go#L35-L61)

### Practice

Add PushCreator without modifying Deliver. Then replace Creator with func() Sender and compare the APIs.

Function injection is smaller for one operation without metadata. An interface fits a creator with Name, configuration, or additional behavior. Both must preserve the creation extension point.

## 02 · Abstract Factory — creational

Replace a family of related products together.

### Problem

Light and dark themes each need matching buttons and checkboxes. Passing unrelated constructors can accidentally mix families.

### Design

Factory supplies both Button and Checkbox. Screen obtains the family from one factory. Factory Method focuses on a creation extension point; Abstract Factory focuses on consistency among several product kinds.

### Walkthrough

Observe that Screen accepts one Factory. The test switches whole families rather than two independent style flags, and concrete product types stay out of Screen.

### Boundary

Go interfaces do not prove that products belong to the same family; the factory contract must guarantee it. With one product kind, family grouping often adds no value.

### Role map

| GoF role | Go implementation |
|---|---|
| Abstract factory | `Factory` |
| Product A | `Button` |
| Product B | `Checkbox` |
| Concrete families | `Light / Dark` |

### Minimal Go

```go
package abstractfactory

type Button interface{ Draw() string }
type Checkbox interface{ Check() string }
type Factory interface {
	Button() Button
	Checkbox() Checkbox
}
type button string

func (b button) Draw() string { return string(b) + ":button" }

type checkbox string

func (c checkbox) Check() string { return string(c) + ":checked" }

type Light struct{}

func (Light) Button() Button     { return button("light") }
func (Light) Checkbox() Checkbox { return checkbox("light") }

type Dark struct{}

func (Dark) Button() Button     { return button("dark") }
func (Dark) Checkbox() Checkbox { return checkbox("dark") }

// Screen obtains related products from the same family factory.
func Screen(f Factory) string { return f.Button().Draw() + "/" + f.Checkbox().Check() }
```

### Test

```go
package abstractfactory

import "testing"

func TestFamilyChangesTogether(t *testing.T) {
	for _, tc := range []struct {
		f    Factory
		want string
	}{{Light{}, "light:button/light:checked"}, {Dark{}, "dark:button/dark:checked"}} {
		if got := Screen(tc.f); got != tc.want {
			t.Fatal(got)
		}
	}
}
```

```sh
go test -v ./abstract-factory
```

### OSS · Go adaptation

database/sql/driver.Conn offers Prepare→Stmt and Begin→Tx. mysqlConn creates mysqlStmt and mysqlTx tied to the same connection, a useful family-factory reading. This is our interpretation, not an upstream GoF claim. Applications should use modern context-aware database/sql APIs, not copy legacy Begin usage.

- [golang/go / src/database/sql/driver/driver.go: L231–L264](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/database/sql/driver/driver.go#L231-L264)
- [go-sql-driver/mysql / connection.go: L145–L164](https://github.com/go-sql-driver/mysql/blob/6a57eb17a8c8f81b4fb5998761cd074d9a7c88b0/connection.go#L145-L164)
- [go-sql-driver/mysql / connection.go: L214–L252](https://github.com/go-sql-driver/mysql/blob/6a57eb17a8c8f81b4fb5998761cd074d9a7c88b0/connection.go#L214-L252)

### Practice

Add a HighContrast family, then intentionally mix a light button and dark checkbox in one factory.

The new family must not change Screen. A mixed factory can still compile, so family consistency needs a contract test; implicit interface satisfaction is not a semantic proof.

## 03 · Builder — creational

Assemble in steps; validate and hand off the product.

### Problem

A plan needs a name and a variable number of steps. A long constructor is awkward, while exposing partial results lets invalid state escape.

### Design

Builder holds construction state. Named/Add return the same builder; Build validates and copies the slice. The product is independently mutable. Caller sequencing fills the Director role here; no separate Director type is needed.

### Walkthrough

An empty builder must fail validation. The test mutates the first product and builds a second, checking that neither product aliases the builder’s mutable slice.

### Boundary

Fluent syntax alone is not Builder. Methods that immediately perform I/O may not separate construction from execution. This mutable builder is not safe for concurrent use.

### Role map

| GoF role | Go implementation |
|---|---|
| Builder | `Builder` |
| Product | `Plan` |
| Steps | `Named / Add` |
| Finalization | `Build` |

### Minimal Go

```go
package builder

import "errors"

type Plan struct {
	Name  string
	Steps []string
}
type Builder struct {
	name  string
	steps []string
}

func (b *Builder) Named(s string) *Builder { b.name = s; return b }
func (b *Builder) Add(s string) *Builder   { b.steps = append(b.steps, s); return b }
func (b *Builder) Build() (Plan, error) {
	if b.name == "" || len(b.steps) == 0 {
		return Plan{}, errors.New("name and steps required")
	}
	return Plan{Name: b.name, Steps: append([]string(nil), b.steps...)}, nil
}
```

### Test

```go
package builder

import "testing"

func TestValidationAndBuiltValueOwnership(t *testing.T) {
	b := new(Builder)
	if _, err := b.Build(); err == nil {
		t.Fatal("invalid plan accepted")
	}
	first, err := b.Named("import").Add("read").Build()
	if err != nil {
		t.Fatal(err)
	}
	first.Steps[0] = "changed by caller"
	second, err := b.Add("write").Build()
	if err != nil {
		t.Fatal(err)
	}
	if second.Steps[0] != "read" || len(second.Steps) != 2 || len(first.Steps) != 1 {
		t.Fatal(first, second)
	}
}
```

```sh
go test -v ./builder
```

### OSS · Direct structure

Kubernetes rest.Request accumulates state through Verb/Resource/Name/Namespace. Several setters retain validation errors in r.err, and Do executes the request. Unlike the lab, the builder and resulting request share one object.

- [kubernetes/kubernetes / staging/src/k8s.io/client-go/rest/request.go: L214–L255](https://github.com/kubernetes/kubernetes/blob/b2ec8b6fefac451a2dedafc4dd71f2f16c7a6abe/staging/src/k8s.io/client-go/rest/request.go#L214-L255)
- [kubernetes/kubernetes / staging/src/k8s.io/client-go/rest/request.go: L332–L368](https://github.com/kubernetes/kubernetes/blob/b2ec8b6fefac451a2dedafc4dd71f2f16c7a6abe/staging/src/k8s.io/client-go/rest/request.go#L332-L368)
- [kubernetes/kubernetes / staging/src/k8s.io/client-go/rest/request.go: L1128–L1147](https://github.com/kubernetes/kubernetes/blob/b2ec8b6fefac451a2dedafc4dd71f2f16c7a6abe/staging/src/k8s.io/client-go/rest/request.go#L1128-L1147)

### Practice

Test reuse after Build, then decide whether Reset is needed.

Reset is optional. If added, it must not affect returned plans. A shallow slice copy is insufficient once products contain mutable nested pointers.

## 04 · Prototype — creational

Clone an existing object with an explicit copy depth.

### Problem

Derive documents from an existing template without letting edits corrupt the original. Struct assignment does not deep-copy slices or maps.

### Design

Clone copies value fields, then the Tags and Labels collections. Their string elements make this sufficient for independent mutation. Pointer-valued entries would require revisiting the copy-depth contract.

### Walkthrough

The test mutates the clone’s slice and map, checks the original, and preserves nil-collection semantics.

### Boundary

GC manages lifetime, not aliasing. A used mutex, connection, or goroutine owner is usually not a cloneable value.

### Role map

| GoF role | Go implementation |
|---|---|
| Prototype | `Document` |
| Clone operation | `Clone` |
| Independent state | `Tags / Labels` |

### Minimal Go

```go
package prototype

import (
	"maps"
	"slices"
)

type Document struct {
	Name   string
	Tags   []string
	Labels map[string]string
}

// Clone preserves nil collections and copies each mutable collection here.
func (d Document) Clone() Document {
	d.Tags = slices.Clone(d.Tags)
	d.Labels = maps.Clone(d.Labels)
	return d
}
```

### Test

```go
package prototype

import "testing"

func TestCloneDoesNotAliasCollections(t *testing.T) {
	original := Document{Name: "base", Tags: []string{"go"}, Labels: map[string]string{"owner": "a"}}
	clone := original.Clone()
	clone.Tags[0] = "java"
	clone.Labels["owner"] = "b"
	if original.Tags[0] != "go" || original.Labels["owner"] != "a" {
		t.Fatal("clone aliases original")
	}
	empty := (Document{}).Clone()
	if empty.Tags != nil || empty.Labels != nil {
		t.Fatal("nil semantics changed")
	}
}
```

```sh
go test -v ./prototype
```

### OSS · Direct structure

Kubernetes Pod.DeepCopy creates a Pod; DeepCopyInto starts with value copying and delegates metadata/spec/status copying. DeepCopyObject returns runtime.Object. This is generated cloning code, not runtime reflection magic.

- [kubernetes/kubernetes / staging/src/k8s.io/api/core/v1/zz_generated.deepcopy.go: L3944–L3970](https://github.com/kubernetes/kubernetes/blob/b2ec8b6fefac451a2dedafc4dd71f2f16c7a6abe/staging/src/k8s.io/api/core/v1/zz_generated.deepcopy.go#L3944-L3970)

### Practice

Change Labels to map[string][]string and expose the new aliasing bug.

maps.Clone copies entries, but nested slices still share arrays. Clone each value slice and add a nested-mutation test.

## 05 · Singleton — creational

Share one instance within an explicit scope.

### Problem

Callers need one read-only default configuration and may access it concurrently for the first time. An unsynchronized nil check races.

### Design

A package-level OnceValue closure constructs once; Default returns the same pointer. No mutation method is exposed. The test checks identity across 32 concurrent calls. The guarantee applies to the Default access path.

### Walkthrough

The OnceValue wrapper itself must be created once. Constructing it inside Default would create a fresh initialization scope on every call.

### Boundary

This is neither a distributed singleton nor a ban on copying Go values or constructing other Settings. Global dependencies hurt test isolation; multiple tenants or configurations usually favor explicit DI.

### Role map

| GoF role | Go implementation |
|---|---|
| Access point | `Default` |
| Shared instance | `Settings` |
| One-time initialization | `sync.OnceValue` |

### Minimal Go

```go
package singleton

import "sync"

// Settings exposes no mutation API; all callers share one package default.
type Settings struct{ endpoint string }

func (s *Settings) Endpoint() string { return s.endpoint }

var instance = sync.OnceValue(func() *Settings { return &Settings{endpoint: "localhost:8080"} })

func Default() *Settings { return instance() }
```

### Test

```go
package singleton

import (
	"sync"
	"testing"
)

func TestConcurrentCallsShareIdentity(t *testing.T) {
	const n = 32
	results := make(chan *Settings, n)
	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func() { defer wg.Done(); results <- Default() }()
	}
	wg.Wait()
	close(results)
	for got := range results {
		if got != Default() || got.Endpoint() != "localhost:8080" {
			t.Fatal("different instance")
		}
	}
}
```

```sh
go test -v ./singleton
```

### OSS · Related mechanism

time.Local uses package-level localLoc and localOnce; get initializes the default lazily. It is a shared default, not enforced single-instance construction: Local can be replaced and other Locations exist. OnceValue supplies the lab’s mechanism.

- [golang/go / src/time/zoneinfo.go: L78–L99](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/time/zoneinfo.go#L78-L99)
- [golang/go / src/sync/oncefunc.go: L43–L75](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/sync/oncefunc.go#L43-L75)

### Practice

Move OnceValue inside Default and observe identity failure; then consider a mutable map in Settings.

Once protects initialization, not subsequent mutation. Shared mutable state still requires synchronization or immutable snapshots.

## 06 · Adapter — structural

Convert an incompatible API into the caller’s contract.

### Problem

A legacy sensor reports Fahrenheit, while new rules require Celsius. Passing the number through can compile and still be wrong.

### Design

Adapter implements Celsius, holds the legacy sensor, and converts units at the boundary. IsBoiling never learns the old API.

### Walkthrough

Tests check 212°F→100°C and 32°F→0°C, proving semantic conversion rather than a method rename.

### Boundary

Adapter changes the contract; Decorator usually preserves it. Units, errors, ownership, and cancellation may need adaptation beyond satisfying an interface.

### Role map

| GoF role | Go implementation |
|---|---|
| Target | `CelsiusSensor` |
| Adaptee | `FahrenheitSensor` |
| Adapter | `Adapter` |

### Minimal Go

```go
package adapter

type FahrenheitSensor interface{ Fahrenheit() float64 }
type CelsiusSensor interface{ Celsius() float64 }
type Adapter struct{ Sensor FahrenheitSensor }

func (a Adapter) Celsius() float64   { return (a.Sensor.Fahrenheit() - 32) * 5 / 9 }
func IsBoiling(s CelsiusSensor) bool { return s.Celsius() >= 100 }
```

### Test

```go
package adapter

import "testing"

type oldSensor float64

func (s oldSensor) Fahrenheit() float64 { return float64(s) }
func TestUnitsAreAdapted(t *testing.T) {
	if !IsBoiling(Adapter{Sensor: oldSensor(212)}) {
		t.Fatal("boiling lost")
	}
	if got := (Adapter{Sensor: oldSensor(32)}).Celsius(); got != 0 {
		t.Fatal(got)
	}
}
```

```sh
go test -v ./adapter
```

### OSS · Direct structure

net/http.HandlerFunc is explicitly described as an adapter: a function type gains ServeHTTP, which calls the underlying function. It adapts callable shape; the lab also illustrates units.

- [golang/go / src/net/http/server.go: L2344–L2353](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/net/http/server.go#L2344-L2353)

### Practice

Add a KelvinSensor adapter without changing IsBoiling.

Keep conversion in the adapter and test reference points. If sources can fail, define the target error contract before adapting them.

## 07 · Bridge — structural

Separate two independently varying dimensions.

### Problem

Normal and quiet remotes must both support TVs and radios. One type per combination multiplies the hierarchy.

### Design

Remote holds Device; QuietRemote reuses that abstraction and combines Volume with Power. Remote behavior and device implementation vary separately, tested across a 2×2 matrix.

### Walkthrough

Read primitive Device operations, then how On/Sleep compose them. A new device should not require a new family of remote types.

### Boundary

Holding an interface is not sufficient: Bridge needs two meaningful axes of variation. Embedding here is composition, not Java-style virtual overriding.

### Role map

| GoF role | Go implementation |
|---|---|
| Abstraction | `Remote` |
| Refined abstraction | `QuietRemote` |
| Implementor | `Device` |
| Implementations | `TV / Radio` |

### Minimal Go

```go
package bridge

import "fmt"

type Device interface {
	Power(bool) string
	Volume(int) string
}
type Remote struct{ Device Device }

func (r Remote) On() string { return r.Device.Power(true) }

type QuietRemote struct{ Remote }

func (r QuietRemote) Sleep() string { return r.Device.Volume(0) + "/" + r.Device.Power(false) }

type TV struct{}

func (TV) Power(on bool) string { return fmt.Sprint("tv:power=", on) }
func (TV) Volume(n int) string  { return fmt.Sprint("tv:volume=", n) }

type Radio struct{}

func (Radio) Power(on bool) string { return fmt.Sprint("radio:power=", on) }
func (Radio) Volume(n int) string  { return fmt.Sprint("radio:volume=", n) }
```

### Test

```go
package bridge

import "testing"

func TestTwoIndependentAxes(t *testing.T) {
	for _, tc := range []struct {
		device Device
		name   string
	}{{TV{}, "tv"}, {Radio{}, "radio"}} {
		remote := Remote{Device: tc.device}
		if got := remote.On(); got != tc.name+":power=true" {
			t.Fatal(got)
		}
		if got := (QuietRemote{Remote: remote}).Sleep(); got != tc.name+":volume=0/"+tc.name+":power=false" {
			t.Fatal(got)
		}
	}
}
```

```sh
go test -v ./bridge
```

### OSS · Go adaptation

Zap Logger holds Core. Higher-level logger behavior is separated from Core’s encoding/output contract, and check delegates to core.Check. These axes support a Bridge-style reading, not a claim about the authors’ taxonomy.

- [uber-go/zap / logger.go: L41–L57](https://github.com/uber-go/zap/blob/bb1a55dd13257cf7cbd06b4146674c67ca614dea/logger.go#L41-L57)
- [uber-go/zap / zapcore/core.go: L23–L45](https://github.com/uber-go/zap/blob/bb1a55dd13257cf7cbd06b4146674c67ca614dea/zapcore/core.go#L23-L45)
- [uber-go/zap / logger.go: L322–L344](https://github.com/uber-go/zap/blob/bb1a55dd13257cf7cbd06b4146674c67ca614dea/logger.go#L322-L344)

### Practice

Add a Speaker device and an AlarmRemote; count the added types.

Add one type per new axis value, not every combination. If Alarm needs an optional capability, define that contract instead of silently ignoring a failed type assertion.

## 08 · Composite — structural

Give leaves and trees the same operation.

### Problem

To calculate directory size, callers should not classify each child as a file or another directory.

### Design

File and Directory implement Size. Directory holds []Node and recursively sums children. Callers invoke root.Size; an empty directory contributes zero.

### Walkthrough

The test combines leaves, nested directories, and an empty directory to verify uniform traversal and aggregation.

### Boundary

The example assumes a finite acyclic tree. Cycles and shared children require explicit policies for counting, visitation, and recursion depth.

### Role map

| GoF role | Go implementation |
|---|---|
| Component | `Node` |
| Leaf | `File` |
| Composite | `Directory` |

### Minimal Go

```go
package composite

// Node represents a finite, acyclic tree. Cycles are outside this contract.
type Node interface{ Size() int }
type File int

func (f File) Size() int { return int(f) }

type Directory []Node

func (d Directory) Size() int {
	total := 0
	for _, child := range d {
		total += child.Size()
	}
	return total
}
```

### Test

```go
package composite

import "testing"

func TestLeafAndNestedTreeUseSameContract(t *testing.T) {
	var root Node = Directory{File(3), Directory{File(4), Directory{}, File(5)}}
	if got := root.Size(); got != 12 {
		t.Fatal(got)
	}
	if (Directory{}).Size() != 0 {
		t.Fatal("empty tree")
	}
}
```

```sh
go test -v ./composite
```

### OSS · Direct structure

errors.Join produces joinError, which is itself an error and holds []error. Error aggregates child messages; Unwrap exposes children. Nested Join values and leaf errors share the error contract.

- [golang/go / src/errors/join.go: L11–L63](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/errors/join.go#L11-L63)

### Practice

Add Count to Node. Which types must change?

Every leaf and composite must implement it. With stable node kinds and many operations, compare Visitor, which moves that extension cost to visitors.

## 09 · Decorator — structural

Add behavior while preserving the contract.

### Problem

Count bytes from any Reader without editing file, buffer, or network implementations.

### Design

CountingReader is an io.Reader. Read delegates, counts the actual n, and returns n/err unchanged. It counts delivered bytes, not the requested buffer length.

### Walkthrough

Study the test returning n>0 and io.EOF together. Checking the error before counting would lose the final bytes.

### Boundary

A wrapper may hide optional capabilities such as WriterTo and does not inherit concurrency safety automatically. It must not silently close a caller-owned source.

### Role map

| GoF role | Go implementation |
|---|---|
| Component | `io.Reader` |
| Concrete component | `Source` |
| Decorator | `CountingReader` |

### Minimal Go

```go
package decorator

import "io"

type CountingReader struct {
	Source io.Reader
	Bytes  int
}

func (r *CountingReader) Read(p []byte) (int, error) {
	n, err := r.Source.Read(p)
	r.Bytes += n
	return n, err
}
```

### Test

```go
package decorator

import (
	"io"
	"strings"
	"testing"
)

type finalChunk struct{}

func (finalChunk) Read(p []byte) (int, error) { return copy(p, "end"), io.EOF }
func TestPreservesBytesAndTerminalError(t *testing.T) {
	reader := &CountingReader{Source: strings.NewReader("hello")}
	got, err := io.ReadAll(reader)
	if err != nil || string(got) != "hello" || reader.Bytes != 5 {
		t.Fatal(string(got), err, reader.Bytes)
	}
	reader = &CountingReader{Source: finalChunk{}}
	n, err := reader.Read(make([]byte, 3))
	if n != 3 || err != io.EOF || reader.Bytes != 3 {
		t.Fatal("lost bytes returned with EOF")
	}
}
```

```sh
go test -v ./decorator
```

### OSS · Direct structure

io.LimitedReader holds a Reader and implements Read, limiting the window and decrementing N while preserving the Reader contract. The lab adds counting instead of limiting.

- [golang/go / src/io/io.go: L458–L482](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/io/io.go#L458-L482)

### Practice

Nest two CountingReaders and verify both count the same bytes.

Each layer observes its actual Read result. Short reads and errors must still count delivered bytes, never len(p).

## 10 · Facade — structural

Expose a caller-oriented entry point over subsystems.

### Problem

Every caller repeats Get→Decode and error wrapping, and must understand subsystem sequencing.

### Design

Service.Load orchestrates Store and Decoder and returns Config. Subsystems need not share an interface, and the facade does not impersonate either subsystem.

### Walkthrough

The test proves decoding is skipped after a load failure and errors.Is still sees the cause; success verifies the orchestration result.

### Boundary

A facade must not grow into a universal Manager. It does not automatically make multi-subsystem operations transactional.

### Role map

| GoF role | Go implementation |
|---|---|
| Facade | `Service.Load` |
| Subsystem A | `Store` |
| Subsystem B | `Decoder` |

### Minimal Go

```go
package facade

import "fmt"

type Config struct{ Address string }
type Store interface{ Get(string) (string, error) }
type Decoder interface{ Decode(string) (Config, error) }
type Service struct {
	Store   Store
	Decoder Decoder
}

func (s Service) Load(key string) (Config, error) {
	raw, err := s.Store.Get(key)
	if err != nil {
		return Config{}, fmt.Errorf("load config: %w", err)
	}
	config, err := s.Decoder.Decode(raw)
	if err != nil {
		return Config{}, fmt.Errorf("decode config: %w", err)
	}
	return config, nil
}
```

### Test

```go
package facade

import (
	"errors"
	"testing"
)

type storeFunc func(string) (string, error)

func (f storeFunc) Get(k string) (string, error) { return f(k) }

type decodeFunc func(string) (Config, error)

func (f decodeFunc) Decode(s string) (Config, error) { return f(s) }
func TestFacadeOrchestratesAndPreservesError(t *testing.T) {
	missing := errors.New("missing")
	decoded := false
	s := Service{Store: storeFunc(func(string) (string, error) { return "", missing }), Decoder: decodeFunc(func(raw string) (Config, error) { decoded = true; return Config{Address: raw}, nil })}
	if _, err := s.Load("key"); !errors.Is(err, missing) || decoded {
		t.Fatal("decode ran after failed load", err)
	}
	s.Store = storeFunc(func(string) (string, error) { return "localhost", nil })
	if got, err := s.Load("key"); err != nil || got.Address != "localhost" {
		t.Fatal(got, err)
	}
}
```

```sh
go test -v ./facade
```

### OSS · Go adaptation

http.Client.Get combines NewRequest and Client.Do into a URL-oriented convenience entry point. It is a small facade-style API; context-aware requests should use NewRequestWithContext followed by Do.

- [golang/go / src/net/http/client.go: L443–L476](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/net/http/client.go#L443-L476)

### Practice

Return a sentinel error from Decoder and check whether the caller can identify it.

Wrap with %w and identify the decode stage. Avoid string matching and do not erase every error identity merely to hide a subsystem.

## 11 · Flyweight — structural

Share intrinsic state; keep per-use context outside.

### Problem

Many glyphs use the same font; duplicating expensive style data for each wastes memory.

### Design

Pool interns one read-only Style per font under a mutex. Glyph Text/X remain extrinsic. Concurrent tests verify canonical identity while glyphs retain separate context.

### Walkthrough

Compare style pointers, then glyph Text/X values. The intrinsic object is shared, not the entire usage context.

### Boundary

This pool retains all keys strongly forever. An unbounded key space leaks useful capacity; mutating a shared style affects every user.

### Role map

| GoF role | Go implementation |
|---|---|
| Flyweight | `Style` |
| Factory | `Pool.Get` |
| Intrinsic state | `font` |
| Extrinsic state | `Glyph.Text / X` |

### Minimal Go

```go
package flyweight

import "sync"

type Style struct{ font string }

func (s *Style) Font() string { return s.font }

// Pool keeps strong references forever; a real unbounded key space needs a policy.
type Pool struct {
	mu     sync.Mutex
	styles map[string]*Style
}

func (p *Pool) Get(font string) *Style {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.styles == nil {
		p.styles = make(map[string]*Style)
	}
	if p.styles[font] == nil {
		p.styles[font] = &Style{font: font}
	}
	return p.styles[font]
}

// Extrinsic data stays outside the shared immutable Style.
type Glyph struct {
	Style *Style
	Text  string
	X     int
}
```

### Test

```go
package flyweight

import (
	"sync"
	"testing"
)

func TestIntrinsicSharingKeepsExtrinsicDataSeparate(t *testing.T) {
	var pool Pool
	var wg sync.WaitGroup
	found := make(chan *Style, 16)
	for i := 0; i < 16; i++ {
		wg.Add(1)
		go func() { defer wg.Done(); found <- pool.Get("mono") }()
	}
	wg.Wait()
	close(found)
	for s := range found {
		if s != pool.Get("mono") {
			t.Fatal("not interned")
		}
	}
	a := Glyph{Style: pool.Get("mono"), Text: "A", X: 1}
	b := Glyph{Style: pool.Get("mono"), Text: "B", X: 9}
	if a.Style != b.Style || a.Text == b.Text || a.X == b.X || pool.Get("serif") == a.Style {
		t.Fatal(a, b)
	}
}
```

```sh
go test -v ./flyweight
```

### OSS · Direct structure

unique.Make returns equal handles for equal comparable values through type-specific canonical maps. Value returns a shallow copy, so referenced data is not automatically immutable. Canonical identity is the relevant mechanism.

- [golang/go / src/unique/handle.go: L15–L53](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/unique/handle.go#L15-L53)

### Practice

Would adding an eviction limit break the same-key/same-pointer promise?

Yes: a caller can retain the old Style while eviction permits another instance. Define the identity scope before choosing caching, interning, or weak references.

## 12 · Proxy — structural

Control access or forwarding through the same contract.

### Problem

Callers need Store.Read, but restricted keys must never reach the backend. Repeating checks at each call site is fragile.

### Design

Guard implements the same Read contract. It checks Allowed before delegation and returns ErrDenied without reaching the subject. The test verifies zero backend calls on rejection.

### Walkthrough

The second assertion returns a backend error, ensuring permission does not get confused with successful reading.

### Boundary

Proxy and Decorator can share a shape; intent differs: access/location/lifecycle control versus added behavior. The key predicate is an example policy, not a complete authentication system.

### Role map

| GoF role | Go implementation |
|---|---|
| Subject | `Store` |
| Real subject | `Next` |
| Proxy | `Guard` |

### Minimal Go

```go
package proxy

import "errors"

var ErrDenied = errors.New("access denied")

type Store interface{ Read(string) (string, error) }
type Guard struct {
	Next    Store
	Allowed func(string) bool
}

func (g Guard) Read(key string) (string, error) {
	if !g.Allowed(key) {
		return "", ErrDenied
	}
	return g.Next.Read(key)
}
```

### Test

```go
package proxy

import (
	"errors"
	"testing"
)

type storeFunc func(string) (string, error)

func (f storeFunc) Read(k string) (string, error) { return f(k) }
func TestDeniedAccessNeverReachesSubject(t *testing.T) {
	calls := 0
	backendErr := errors.New("offline")
	var store Store = Guard{Allowed: func(k string) bool { return k == "public" }, Next: storeFunc(func(string) (string, error) { calls++; return "", backendErr })}
	if _, err := store.Read("secret"); !errors.Is(err, ErrDenied) || calls != 0 {
		t.Fatal(err, calls)
	}
	if _, err := store.Read("public"); !errors.Is(err, backendErr) || calls != 1 {
		t.Fatal(err, calls)
	}
}
```

```sh
go test -v ./proxy
```

### OSS · Direct structure

httputil.ReverseProxy implements ServeHTTP and forwards through Transport.RoundTrip. It is a remote proxy; the lab is a protection proxy. Both preserve a caller-facing contract with different policies.

- [golang/go / src/net/http/httputil/reverseproxy.go: L421–L450](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/net/http/httputil/reverseproxy.go#L421-L450)
- [golang/go / src/net/http/httputil/reverseproxy.go: L587–L604](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/net/http/httputil/reverseproxy.go#L587-L604)

### Practice

Add an audit function that distinguishes denial from backend failure.

Never call the backend before enforcing denial. Define whether audit failure affects the result, or the wrapper can silently change availability.

## 13 · Chain of Responsibility — behavioral

Pass a request along a chain until a handler accepts it.

### Problem

Several handlers might accept a path. The caller should not choose one, and not every handler should perform a side effect.

### Design

Each Handler returns a response and handled flag. Dispatch stops at the first acceptance and returns ErrUnhandled otherwise. A central loop expresses successor traversal without per-handler linked objects.

### Walkthrough

The test places a skipped handler before an accepting one and an unreachable one after it. Two calls prove short-circuiting.

### Boundary

Order is part of behavior. This differs from an all-stage pipeline and from Observer fan-out.

### Role map

| GoF role | Go implementation |
|---|---|
| Request | `Request` |
| Handlers | `Handler functions` |
| Chain owner | `Dispatch` |

### Minimal Go

```go
package chainofresponsibility

import "errors"

var ErrUnhandled = errors.New("no handler accepted request")

type Request struct{ Path string }
type Handler func(Request) (response string, handled bool)

func Dispatch(request Request, chain ...Handler) (string, error) {
	for _, handler := range chain {
		if response, ok := handler(request); ok {
			return response, nil
		}
	}
	return "", ErrUnhandled
}
```

### Test

```go
package chainofresponsibility

import (
	"errors"
	"testing"
)

func TestDelegationStopsAfterAcceptance(t *testing.T) {
	calls := 0
	skip := func(Request) (string, bool) { calls++; return "", false }
	accept := func(Request) (string, bool) { calls++; return "ok", true }
	got, err := Dispatch(Request{Path: "/"}, skip, accept, skip)
	if err != nil || got != "ok" || calls != 2 {
		t.Fatal(got, err, calls)
	}
	if _, err := Dispatch(Request{}); !errors.Is(err, ErrUnhandled) {
		t.Fatal(err)
	}
}
```

```sh
go test -v ./chain-of-responsibility
```

### OSS · Go adaptation

Gin Next advances the handler index, while Abort prevents pending handlers. Middleware before/after behavior means it is not the lab’s strict first-handler-wins chain. Abort does not return from the current function.

- [gin-gonic/gin / context.go: L195–L219](https://github.com/gin-gonic/gin/blob/dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9/context.go#L195-L219)

### Practice

Add a fallback handler and move it before the others.

An always-accepting fallback must come last or later handlers become unreachable. Preserve the intended order with a behavior test.

## 14 · Command — behavioral

Represent an action as a value for deferred execution.

### Problem

A UI, CLI, or job queue wants to assemble actions for later invocation instead of editing the document immediately.

### Design

Replace stores its receiver and parameters; Execute performs the mutation. Run knows only Command and stops on error, separating construction from execution in time.

### Walkthrough

The test verifies construction leaves the document unchanged, execution updates it, and a failed command prevents later actions.

### Boundary

Command does not imply undo, retry safety, or a durable queue. A receiver pointer is not a snapshot. Serializable jobs need an explicit payload contract.

### Role map

| GoF role | Go implementation |
|---|---|
| Command | `Command` |
| Concrete command | `Replace` |
| Receiver | `Document` |
| Invoker | `Run` |

### Minimal Go

```go
package command

import "fmt"

type Command interface{ Execute() error }
type Document struct{ Text string }
type Replace struct {
	Target *Document
	Text   string
}

func (c Replace) Execute() error { c.Target.Text = c.Text; return nil }
func Run(queue []Command) error {
	for i, c := range queue {
		if err := c.Execute(); err != nil {
			return fmt.Errorf("command %d: %w", i, err)
		}
	}
	return nil
}
```

### Test

```go
package command

import (
	"errors"
	"testing"
)

type commandFunc func() error

func (f commandFunc) Execute() error { return f() }
func TestDeferredExecutionAndFailureBoundary(t *testing.T) {
	doc := &Document{Text: "old"}
	queue := []Command{Replace{Target: doc, Text: "new"}}
	if doc.Text != "old" {
		t.Fatal("construction executed command")
	}
	if err := Run(queue); err != nil || doc.Text != "new" {
		t.Fatal(doc, err)
	}
	stop := errors.New("stop")
	queue = []Command{commandFunc(func() error { return stop }), Replace{Target: doc, Text: "wrong"}}
	if err := Run(queue); !errors.Is(err, stop) || doc.Text != "new" {
		t.Fatal(doc, err)
	}
}
```

```sh
go test -v ./command
```

### OSS · Direct structure

Cobra Command stores Run/RunE actions and execute invokes them after parsing and validation. The action encapsulation is clear; RunE failure skips later public post hooks and does not automatically roll back the action.

- [spf13/cobra / command.go: L1014–L1044](https://github.com/spf13/cobra/blob/adbc8813901bba65827259daa8e22ff94ec1f30e/command.go#L1014-L1044)

### Practice

Add undo to Replace; compare storing old text with storing a Memento.

Command captures what to do; Memento captures prior state. History, repeated execution, and partial failure need contracts beyond adding Undo.

## 15 · Interpreter — behavioral

Represent and evaluate a small language as an expression tree.

### Problem

Authorization rules need variables, AND, and NOT without hard-coding another conditional for every rule.

### Design

Expr.Eval takes an environment. Terminals resolve values; nonterminals evaluate children. And short-circuits, and unknown variables fail. The example begins with an AST and contains no parser.

### Walkthrough

The rule is admin AND NOT blocked. Tests cover truth, an unknown variable, and short-circuiting false AND missing.

### Boundary

An expression language is not automatically a sandbox. Real DSLs need grammar, type checking, and resource limits. A single fixed rule is often clearer as a function.

### Role map

| GoF role | Go implementation |
|---|---|
| Expression | `Expr` |
| Terminals | `Var / Literal` |
| Nonterminals | `And / Not` |
| Context | `Env` |

### Minimal Go

```go
package interpreter

import "fmt"

type Env map[string]bool
type Expr interface{ Eval(Env) (bool, error) }
type Var string

func (v Var) Eval(env Env) (bool, error) {
	value, ok := env[string(v)]
	if !ok {
		return false, fmt.Errorf("unknown variable %s", v)
	}
	return value, nil
}

type Literal bool

func (l Literal) Eval(Env) (bool, error) { return bool(l), nil }

type And struct{ Left, Right Expr }

func (a And) Eval(env Env) (bool, error) {
	left, err := a.Left.Eval(env)
	if err != nil || !left {
		return left, err
	}
	return a.Right.Eval(env)
}

type Not struct{ Inner Expr }

func (n Not) Eval(env Env) (bool, error) {
	v, err := n.Inner.Eval(env)
	if err != nil {
		return false, err
	}
	return !v, nil
}
```

### Test

```go
package interpreter

import "testing"

func TestExpressionSemanticsAndShortCircuit(t *testing.T) {
	rule := And{Left: Var("admin"), Right: Not{Inner: Var("blocked")}}
	if got, err := rule.Eval(Env{"admin": true, "blocked": false}); err != nil || !got {
		t.Fatal(got, err)
	}
	if got, err := (And{Left: Literal(false), Right: Var("missing")}).Eval(nil); err != nil || got {
		t.Fatal("not short circuited")
	}
	if _, err := Var("missing").Eval(nil); err == nil {
		t.Fatal("unknown silently accepted")
	}
}
```

```sh
go test -v ./interpreter
```

### OSS · Go adaptation

text/template represents syntax with parse.Node, and exec.state.walk interprets actions, lists, conditionals, and text through a central type switch. This is tree interpretation in Go rather than per-node Interpret methods.

- [golang/go / src/text/template/exec.go: L260–L295](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/text/template/exec.go#L260-L295)
- [golang/go / src/text/template/parse/node.go: L17–L33](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/text/template/parse/node.go#L17-L33)

### Practice

Add Or and verify true OR missing skips the right side.

Short-circuiting is language semantics. Evaluating both operands first changes error and side-effect behavior.

## 16 · Iterator — behavioral

Hide collection representation behind stoppable traversal.

### Problem

Callers need ordered traversal without knowing whether storage is a slice, tree, or pages.

### Design

All returns iter.Seq. The producer calls yield and immediately stops on false. New copies the input slice, isolating traversal from caller mutation; each All call can traverse afresh.

### Walkthrough

The test stops on the first yield, then collects a full traversal, showing that early exit does not exhaust future traversals.

### Boundary

Go 1.23 range-over-function uses push iteration. Never call yield after it returns false. Database cursors also need resource, Close, and error contracts.

### Role map

| GoF role | Go implementation |
|---|---|
| Aggregate | `Collection` |
| Iterator | `iter.Seq[string]` |
| Consumer | `yield / range` |

### Minimal Go

```go
package iterator

import (
	"iter"
	"slices"
)

type Collection struct{ values []string }

func New(values []string) Collection { return Collection{values: slices.Clone(values)} }
func (c Collection) All() iter.Seq[string] {
	return func(yield func(string) bool) {
		for _, value := range c.values {
			if !yield(value) {
				return
			}
		}
	}
}
```

### Test

```go
package iterator

import (
	"slices"
	"testing"
)

func TestEarlyStopAndIndependentTraversals(t *testing.T) {
	source := []string{"a", "b", "c"}
	c := New(source)
	source[0] = "mutated"
	calls := 0
	c.All()(func(s string) bool {
		calls++
		if s != "a" {
			t.Fatal(s)
		}
		return false
	})
	if calls != 1 {
		t.Fatal("ignored stop")
	}
	if got := slices.Collect(c.All()); !slices.Equal(got, []string{"a", "b", "c"}) {
		t.Fatal(got)
	}
	for range c.All() {
		break
	}
}
```

```sh
go test -v ./iterator
```

### OSS · Go adaptation

google/btree Ascend accepts a callback with early-stop semantics; Go ast.Preorder returns iter.Seq. Both separate traversal from consumption. google/btree is archived in this snapshot and is a source-study example, not an active-maintenance recommendation.

- [google/btree / btree.go: L747–L781](https://github.com/google/btree/blob/aeba20f7a1e1315badec4eca4fdc9f754f5f880a/btree.go#L747-L781)
- [golang/go / src/go/ast/walk.go: L380–L397](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/go/ast/walk.go#L380-L397)

### Practice

Add a Filter sequence that propagates a downstream break to the original producer.

Yield only matching elements and stop upstream immediately when downstream returns false. Check producer call counts, not only the resulting values.

## 17 · Mediator — behavioral

Put colleague interaction rules in a coordinator.

### Problem

Members exchange messages; direct references among all of them scatter relationships and routing policy.

### Design

Members know only Room. Send delegates intent; route selects the recipient and delivers. Join rejects duplicate names, and Messages copies the inbox.

### Walkthrough

Member a sends to b without holding b’s pointer. Tests cover unknown recipients, duplicate identity, and inbox ownership.

### Boundary

Mediator is neither Observer broadcast nor any arbitrary event bus. It should own coordination policy, not merely forward. This minimal Room is single-owner.

### Role map

| GoF role | Go implementation |
|---|---|
| Mediator | `Room` |
| Colleagues | `Member` |
| Coordination | `route` |

### Minimal Go

```go
package mediator

import (
	"fmt"
	"slices"
)

// Room and Members have a single owner; concurrent use needs a separate policy.
type Room struct{ members map[string]*Member }
type Member struct {
	room  *Room
	name  string
	inbox []string
}

func (r *Room) Join(name string) (*Member, error) {
	if r.members == nil {
		r.members = make(map[string]*Member)
	}
	if _, exists := r.members[name]; exists {
		return nil, fmt.Errorf("duplicate member %s", name)
	}
	m := &Member{room: r, name: name}
	r.members[name] = m
	return m, nil
}
func (m *Member) Send(to, text string) error { return m.room.route(m.name, to, text) }
func (m *Member) Messages() []string         { return slices.Clone(m.inbox) }
func (r *Room) route(from, to, text string) error {
	recipient, ok := r.members[to]
	if !ok {
		return fmt.Errorf("unknown recipient %s", to)
	}
	recipient.inbox = append(recipient.inbox, from+":"+text)
	return nil
}
```

### Test

```go
package mediator

import "testing"

func TestColleaguesCommunicateThroughMediator(t *testing.T) {
	var room Room
	a, err := room.Join("a")
	if err != nil {
		t.Fatal(err)
	}
	b, err := room.Join("b")
	if err != nil {
		t.Fatal(err)
	}
	if err := a.Send("b", "hi"); err != nil {
		t.Fatal(err)
	}
	got := b.Messages()
	if len(got) != 1 || got[0] != "a:hi" || len(a.Messages()) != 0 {
		t.Fatal(got)
	}
	got[0] = "mutated"
	if b.Messages()[0] != "a:hi" {
		t.Fatal("inbox leaked")
	}
	if err := a.Send("absent", "hi"); err == nil {
		t.Fatal("unknown recipient accepted")
	}
	if _, err := room.Join("a"); err == nil {
		t.Fatal("duplicate accepted")
	}
}
```

```sh
go test -v ./mediator
```

### OSS · Related mechanism

fzf EventBox centralizes events: Set updates by type and wakes a waiter; Wait exposes a batch to a callback. It demonstrates centralized coordination, but not this example’s colleague routing protocol. It is a related mechanism, not evidence of a complete classic Mediator.

- [junegunn/fzf / src/util/eventbox.go: L8–L54](https://github.com/junegunn/fzf/blob/52f4319a72c17e123396cc3a2e6abf2e96e9d753/src/util/eventbox.go#L8-L54)

### Practice

Add a rule allowing a member to send only within its group.

Keep group-routing policy in the mediator rather than every Member. A Room that also owns billing, storage, and UI has grown beyond a useful boundary.

## 18 · Memento — behavioral

Save restorable state without exposing its representation.

### Problem

An editor needs undo, while its history manager should not inspect or mutate internal fields.

### Design

Save returns a Snapshot with unexported fields. Editor restores itself and rejects foreign snapshots. The caretaker stores the token without interpreting it.

### Walkthrough

The test saves, edits, restores, reuses the snapshot, and rejects a different owner.

### Boundary

Memento is not a durable transaction or necessarily a full deep copy. Large state can use journals, copy-on-write, or revision tokens; snapshots extend referenced lifetimes.

### Role map

| GoF role | Go implementation |
|---|---|
| Originator | `Editor` |
| Memento | `Snapshot` |
| Caretaker | `caller / history` |

### Minimal Go

```go
package memento

import "errors"

type Editor struct{ text string }
type Snapshot struct {
	owner *Editor
	text  string
}

func (e *Editor) Set(s string)   { e.text = s }
func (e *Editor) Text() string   { return e.text }
func (e *Editor) Save() Snapshot { return Snapshot{owner: e, text: e.text} }
func (e *Editor) Restore(s Snapshot) error {
	if s.owner != e {
		return errors.New("snapshot belongs to another editor")
	}
	e.text = s.text
	return nil
}
```

### Test

```go
package memento

import "testing"

func TestOpaqueSnapshotAndOriginatorBoundary(t *testing.T) {
	var editor Editor
	editor.Set("before")
	saved := editor.Save()
	editor.Set("after")
	if err := editor.Restore(saved); err != nil || editor.Text() != "before" {
		t.Fatal(editor.Text(), err)
	}
	var other Editor
	if err := other.Restore(saved); err == nil {
		t.Fatal("foreign snapshot accepted")
	}
	editor.Set("again")
	if err := editor.Restore(saved); err != nil || editor.Text() != "before" {
		t.Fatal("snapshot mutated")
	}
	if err := editor.Restore(Snapshot{}); err == nil {
		t.Fatal("invalid snapshot accepted")
	}
}
```

```sh
go test -v ./memento
```

### OSS · Go adaptation

go-ethereum StateDB.Snapshot returns a revision ID. Its journal records an entry index and replays undo entries on RevertToSnapshot. This incremental design keeps state out of the token and invalidates reverted revisions, unlike the lab’s reusable snapshot.

- [ethereum/go-ethereum / core/state/statedb.go: L761–L769](https://github.com/ethereum/go-ethereum/blob/d799b1a3f20fe9d983bfff6cc53fb246bcab1298/core/state/statedb.go#L761-L769)
- [ethereum/go-ethereum / core/state/journal.go: L213–L235](https://github.com/ethereum/go-ethereum/blob/d799b1a3f20fe9d983bfff6cc53fb246bcab1298/core/state/journal.go#L213-L235)

### Practice

Add two undo levels and decide whether restoring an older snapshot preserves newer history.

Choose explicit history semantics: branching, truncation, or redo. Do not generalize the lab’s immutable-token behavior to journal systems.

## 19 · Observer — behavioral

Notify a set of subscribers from one event source.

### Problem

Independent consumers need state-change notifications without hard-coding their types in the subject.

### Design

Subscribe stores a callback and returns unsubscribe. Publish snapshots listeners under lock and invokes them after unlocking, allowing callbacks to unsubscribe without reentrant deadlock.

### Walkthrough

Self-unsubscription receives only the first event. Parallel publication uses an atomic counter because callbacks themselves can run concurrently.

### Boundary

Snapshot semantics cannot revoke an already selected callback. Delivery order is unspecified, slow observers block synchronous publication, and callback panics affect publishers.

### Role map

| GoF role | Go implementation |
|---|---|
| Subject | `Topic` |
| Observers | `callback functions` |
| Registration | `Subscribe` |
| Notification | `Publish` |

### Minimal Go

```go
package observer

import "sync"

type Topic struct {
	mu        sync.Mutex
	next      int
	listeners map[int]func(string)
}

func (t *Topic) Subscribe(fn func(string)) func() {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.listeners == nil {
		t.listeners = make(map[int]func(string))
	}
	id := t.next
	t.next++
	t.listeners[id] = fn
	return func() { t.mu.Lock(); defer t.mu.Unlock(); delete(t.listeners, id) }
}

// Delivery is synchronous, unordered, and uses a listener snapshot.
// Unsubscribe cannot revoke a callback already included in a snapshot.
func (t *Topic) Publish(value string) {
	t.mu.Lock()
	snapshot := make([]func(string), 0, len(t.listeners))
	for _, fn := range t.listeners {
		snapshot = append(snapshot, fn)
	}
	t.mu.Unlock()
	for _, fn := range snapshot {
		fn(value)
	}
}
```

### Test

```go
package observer

import (
	"sync"
	"sync/atomic"
	"testing"
)

func TestListenerCanUnsubscribeInsideCallback(t *testing.T) {
	var topic Topic
	calls := 0
	var unsubscribe func()
	unsubscribe = topic.Subscribe(func(string) { calls++; unsubscribe() })
	topic.Publish("one")
	topic.Publish("two")
	unsubscribe()
	if calls != 1 {
		t.Fatal(calls)
	}
}
func TestConcurrentPublication(t *testing.T) {
	var topic Topic
	var calls atomic.Int64
	stop := topic.Subscribe(func(string) { calls.Add(1) })
	defer stop()
	var wg sync.WaitGroup
	for i := 0; i < 16; i++ {
		wg.Add(1)
		go func() { defer wg.Done(); topic.Publish("event") }()
	}
	wg.Wait()
	if calls.Load() != 16 {
		t.Fatal(calls.Load())
	}
}
```

```sh
go test -v ./observer
```

### OSS · Direct structure

Kubernetes sharedProcessor registers processorListeners and distributes through listener.add, distinguishing sync from ordinary events. Informers have their own listener lifecycle and queues; the lab’s synchronous callback latency contract does not apply unchanged.

- [kubernetes/kubernetes / staging/src/k8s.io/client-go/tools/cache/shared_informer.go: L1190–L1208](https://github.com/kubernetes/kubernetes/blob/b2ec8b6fefac451a2dedafc4dd71f2f16c7a6abe/staging/src/k8s.io/client-go/tools/cache/shared_informer.go#L1190-L1208)
- [kubernetes/kubernetes / staging/src/k8s.io/client-go/tools/cache/shared_informer.go: L1236–L1259](https://github.com/kubernetes/kubernetes/blob/b2ec8b6fefac451a2dedafc4dd71f2f16c7a6abe/staging/src/k8s.io/client-go/tools/cache/shared_informer.go#L1236-L1259)

### Practice

Register a new observer during notification. Should it receive the current event?

With this snapshot design, the new observer starts on the next publication. Test that boundary; strict ordering needs a different data structure and concurrency contract.

## 20 · State — behavioral

Let internal state change the behavior of the same action.

### Problem

A gate rejects Pass while locked and allows it while unlocked, then relocks. A large switch in every action duplicates transitions.

### Design

Gate delegates Coin/Pass to its current mode. States decide results and transitions. The client does not select an algorithm explicitly.

### Walkthrough

The test checks rejection, coin insertion, one pass, then rejection again. A second coin does not accumulate another pass in this policy.

### Boundary

State and Strategy can look alike. Callers usually choose strategies; internal transitions drive states. A small enum switch can be clearer than many state types.

### Role map

| GoF role | Go implementation |
|---|---|
| Context | `Gate` |
| State interface | `mode` |
| Concrete states | `locked / unlocked` |

### Minimal Go

```go
package state

import "errors"

var ErrLocked = errors.New("gate is locked")

type mode interface {
	coin(*Gate)
	pass(*Gate) error
}
type Gate struct{ current mode }

func New() *Gate            { return &Gate{current: locked{}} }
func (g *Gate) Coin()       { g.current.coin(g) }
func (g *Gate) Pass() error { return g.current.pass(g) }

type locked struct{}

func (locked) coin(g *Gate)     { g.current = unlocked{} }
func (locked) pass(*Gate) error { return ErrLocked }

type unlocked struct{}

func (unlocked) coin(*Gate)         {}
func (unlocked) pass(g *Gate) error { g.current = locked{}; return nil }
```

### Test

```go
package state

import (
	"errors"
	"testing"
)

func TestBehaviorChangesWithInternalState(t *testing.T) {
	g := New()
	if err := g.Pass(); !errors.Is(err, ErrLocked) {
		t.Fatal(err)
	}
	g.Coin()
	g.Coin()
	if err := g.Pass(); err != nil {
		t.Fatal(err)
	}
	if err := g.Pass(); !errors.Is(err, ErrLocked) {
		t.Fatal("did not return to locked state")
	}
}
```

```sh
go test -v ./state
```

### OSS · Go adaptation

text/template defines stateFn func(*lexer) stateFn. nextItem executes state(l), and lexText returns a successor such as lexLeftDelim. Functions replace the lab’s state objects: a natural Go adaptation.

- [golang/go / src/text/template/parse/lex.go: L109–L126](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/text/template/parse/lex.go#L109-L126)
- [golang/go / src/text/template/parse/lex.go: L224–L238](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/text/template/parse/lex.go#L224-L238)
- [golang/go / src/text/template/parse/lex.go: L270–L298](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/text/template/parse/lex.go#L270-L298)

### Practice

Add a maintenance state and specify who can enter or leave it.

Transition authorization belongs in the context API, not a publicly mutable state field. Serialize actions at the context boundary if used concurrently.

## 21 · Strategy — behavioral

Keep the workflow; replace an algorithmic decision.

### Problem

The same values need ascending or descending order. Duplicating the sorting implementation creates unnecessary maintenance.

### Design

Sorted accepts Less and adapts it to a three-way comparator. The function is the strategy; lambdas do not need extra structs. Input copying prevents caller mutation.

### Walkthrough

The test uses one Sorted implementation with two comparators, duplicate values, and an input-ownership check.

### Boundary

Less must define a strict weak ordering. A comparator changing with call count violates the algorithm contract. Stateful strategies need reuse and concurrency rules.

### Role map

| GoF role | Go implementation |
|---|---|
| Context | `Sorted` |
| Strategy | `Less` |
| Concrete strategies | `ascending / descending functions` |

### Minimal Go

```go
package strategy

import "slices"

type Less func(a, b int) bool

// Sorted accepts a strict weak ordering and leaves the input unchanged.
func Sorted(input []int, less Less) []int {
	result := slices.Clone(input)
	slices.SortFunc(result, func(a, b int) int {
		if less(a, b) {
			return -1
		}
		if less(b, a) {
			return 1
		}
		return 0
	})
	return result
}
```

### Test

```go
package strategy

import (
	"slices"
	"testing"
)

func TestReplaceOrderingWithoutChangingAlgorithm(t *testing.T) {
	source := []int{3, 1, 2, 2}
	up := Sorted(source, func(a, b int) bool { return a < b })
	down := Sorted(source, func(a, b int) bool { return a > b })
	if !slices.Equal(up, []int{1, 2, 2, 3}) || !slices.Equal(down, []int{3, 2, 2, 1}) || source[0] != 3 {
		t.Fatal(source, up, down)
	}
}
```

```sh
go test -v ./strategy
```

### OSS · Direct structure

BackOff exposes NextBackOff/Reset; Retry asks the policy for a delay, with Zero/Stop/Constant implementations. The source pins a v7 branch snapshot; do not mix APIs from other major versions.

- [cenkalti/backoff / backoff.go: L19–L70](https://github.com/cenkalti/backoff/blob/ffcfd8ab39e2910a1180ba0b7a02a52f0485adc9/backoff.go#L19-L70)
- [cenkalti/backoff / retry.go: L153–L172](https://github.com/cenkalti/backoff/blob/ffcfd8ab39e2910a1180ba0b7a02a52f0485adc9/retry.go#L153-L172)

### Practice

Sort by absolute value with an explicit tie-breaker.

Compare magnitude, then original value consistently. Negating the minimum int overflows, so use a safe magnitude representation or restrict the input domain.

## 22 · Template Method — behavioral

Fix the algorithm skeleton; expose selected hooks.

### Problem

Import workflows share Load→Transform→Save but vary each step, while ordering and failure handling must stay consistent.

### Design

Run owns sequencing and error wrapping; Steps supplies operations. Errors skip later stages, with cancellation checks at entry and before Save. Composition replaces overridable methods on an abstract base class.

### Walkthrough

Tests verify the full trace, skipping Save after transform failure, and skipping Load when already canceled. I/O hooks must cooperate with Context; this example’s Transform has no Context parameter.

### Boundary

Template fixes a multi-step flow; Strategy replaces a decision or algorithm. Go embedding is not inheritance-based virtual dispatch to outer shadowing methods.

### Role map

| GoF role | Go implementation |
|---|---|
| Template | `Run` |
| Primitive operations | `Steps` |
| Hooks | `Load / Transform / Save` |

### Minimal Go

```go
package templatemethod

import (
	"context"
	"fmt"
)

type Steps interface {
	Load(context.Context) ([]byte, error)
	Transform([]byte) ([]byte, error)
	Save(context.Context, []byte) error
}

// Run owns ordering and failure policy; injected Steps only fill the hooks.
func Run(ctx context.Context, s Steps) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	raw, err := s.Load(ctx)
	if err != nil {
		return fmt.Errorf("load: %w", err)
	}
	value, err := s.Transform(raw)
	if err != nil {
		return fmt.Errorf("transform: %w", err)
	}
	if err := ctx.Err(); err != nil {
		return err
	}
	if err := s.Save(ctx, value); err != nil {
		return fmt.Errorf("save: %w", err)
	}
	return nil
}
```

### Test

```go
package templatemethod

import (
	"context"
	"errors"
	"slices"
	"testing"
)

type hooks struct {
	trace []string
	fail  error
	saved string
}

func (h *hooks) Load(context.Context) ([]byte, error) {
	h.trace = append(h.trace, "load")
	return []byte("raw"), nil
}
func (h *hooks) Transform(b []byte) ([]byte, error) {
	h.trace = append(h.trace, "transform")
	return append(b, '!'), h.fail
}
func (h *hooks) Save(_ context.Context, b []byte) error {
	h.trace = append(h.trace, "save")
	h.saved = string(b)
	return nil
}
func TestSkeletonOwnsOrderAndShortCircuit(t *testing.T) {
	h := new(hooks)
	if err := Run(context.Background(), h); err != nil || h.saved != "raw!" || !slices.Equal(h.trace, []string{"load", "transform", "save"}) {
		t.Fatal(h, err)
	}
	stop := errors.New("stop")
	h = &hooks{fail: stop}
	if err := Run(context.Background(), h); !errors.Is(err, stop) || !slices.Equal(h.trace, []string{"load", "transform"}) {
		t.Fatal(h, err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	h = new(hooks)
	if err := Run(ctx, h); !errors.Is(err, context.Canceled) || len(h.trace) != 0 {
		t.Fatal(h, err)
	}
}
```

```sh
go test -v ./template-method
```

### OSS · Go adaptation

Backoff Retry fixes operation→classification→budget→delay→wait while accepting operation and BackOff extension points. It combines template-style control inversion with Strategy; one implementation can express multiple patterns.

- [cenkalti/backoff / retry.go: L108–L188](https://github.com/cenkalti/backoff/blob/ffcfd8ab39e2910a1180ba0b7a02a52f0485adc9/retry.go#L108-L188)

### Practice

Make Transform cancellation-aware and prevent Save after cancellation.

Pass Context to Transform and require cooperation while keeping boundary checks. Cancellation can still race after a check; the I/O must use the same Context.

## 23 · Visitor — behavioral

Add operations externally when node kinds are stable.

### Problem

The same AST needs evaluation and counting. Continually adding methods makes nodes own too many operations.

### Design

Accept dispatches by the concrete node type to Visitor.Number/Add; the visitor supplies behavior. This is explicit double dispatch. Eval and Count add operations without nodes knowing concrete visitors.

### Walkthrough

One tree evaluates to 6 and contains 5 nodes; evaluation remains unchanged afterward. Compare Interpreter, where evaluation lives on expressions instead of visitors.

### Boundary

New visitors are easy; new node kinds require changing the Visitor interface and all visitors. This selects an extension axis rather than solving every open/closed problem.

### Role map

| GoF role | Go implementation |
|---|---|
| Element | `Node` |
| Concrete elements | `Number / Add` |
| Visitor | `Visitor` |
| Operations | `Eval / Count` |

### Minimal Go

```go
package visitor

type Node interface{ Accept(Visitor) int }
type Visitor interface {
	Number(Number) int
	Add(Add) int
}
type Number int

func (n Number) Accept(v Visitor) int { return v.Number(n) }

type Add struct{ Left, Right Node }

func (a Add) Accept(v Visitor) int { return v.Add(a) }

type Eval struct{}

func (Eval) Number(n Number) int { return int(n) }
func (v Eval) Add(a Add) int     { return a.Left.Accept(v) + a.Right.Accept(v) }

type Count struct{}

func (Count) Number(Number) int { return 1 }
func (v Count) Add(a Add) int   { return 1 + a.Left.Accept(v) + a.Right.Accept(v) }
```

### Test

```go
package visitor

import "testing"

func TestAddOperationsWithoutChangingNodes(t *testing.T) {
	var tree Node = Add{Left: Number(1), Right: Add{Left: Number(2), Right: Number(3)}}
	if got := tree.Accept(Eval{}); got != 6 {
		t.Fatal(got)
	}
	if got := tree.Accept(Count{}); got != 5 {
		t.Fatal(got)
	}
	if got := tree.Accept(Eval{}); got != 6 {
		t.Fatal("visitor mutated tree")
	}
}
```

```sh
go test -v ./visitor
```

### OSS · Go adaptation

go/ast explicitly provides Visitor and Walk. A single Visit(Node) combines with a type switch for child traversal; nil prunes and Visit(nil) signals exit. Unlike the lab, it does not use a typed visitor method per node kind.

- [golang/go / src/go/ast/walk.go: L12–L41](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/go/ast/walk.go#L12-L41)
- [golang/go / src/go/ast/walk.go: L350–L377](https://github.com/golang/go/blob/c5941983810b68ba93c30f0ef22c91ad63fb3e5c/src/go/ast/walk.go#L350-L377)

### Practice

Add expression printing, then add Multiply and compare the change surface.

Printing needs a result-contract decision because this Visitor returns int, or the visitor must hold output state. Multiply requires updating nodes and every visitor, exposing the pattern’s extension cost.
