Already know Java, Scala, Python, or C++? Start with the problem, role mapping, and failure tests. See what Go preserves—and simplifies—from classic GoF.
No match. Try clone, ownership, or a repository name.
Distinguish similar designs first
Factory Method / Abstract Factory
Replace a creation operation, or replace a related product family together.
Adapter / Decorator / Proxy
Convert a contract; add behavior; control access. Similar shapes, different intent.
Strategy / State / Template Method
Caller-selected algorithm; internal state transitions; a fixed skeleton with hooks.
Command / Memento / Observer
Encapsulate an action; preserve prior state; propagate an event. None implies a transaction.
Run it, then break an assumption
Go 1.23+, standard library only. Download and extract the ZIP, then run this in gof-min. The Test tab shows those exact tests.
go test -race ./...
All implementations and tests are original, runnable minimal examples. Mutable objects are single-owner except where Singleton, Flyweight, and Observer explicitly cover concurrency. Callers provide valid non-nil dependencies. These are not production frameworks.
Direct structure: roles and mechanism align clearly. Go adaptation: the intent appears through composition, functions, or a different representation. Related mechanism: useful comparison, insufficient for a complete classic-pattern claim. These mappings are our analysis.
Delegate product creation to a replaceable creator.
Start with the problem
The delivery workflow is stable, but deployments need email or SMS. Constructing a concrete sender inside the workflow hard-codes that choice.
The minimal 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.
CreatorCreator
Factory operationNewSender
ProductSender
Concrete productsemail / sms
Read the code in this order
Read Creator.NewSender, then the two calls in Deliver, then the concrete creators. The table test passes both through the same caller contract.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 23 lines
packagefactorymethodtypeSenderinterface{Send(string)string}typeCreatorinterface{NewSender()Sender}// Deliver depends on a creation operation, not a concrete product.funcDeliver(cCreator,messagestring)string{returnc.NewSender().Send(message)}typeEmailCreatorstruct{}func(EmailCreator)NewSender()Sender{returnemail{}}typeSMSCreatorstruct{}func(SMSCreator)NewSender()Sender{returnsms{}}typeemailstruct{}func(email)Send(sstring)string{return"email:"+s}typesmsstruct{}func(sms)Send(sstring)string{return"sms:"+s}
go test -v ./factory-methodRun inside the extracted gof-min directory
Verify behavior, not just compilation
Read Creator.NewSender, then the two calls in Deliver, then the concrete creators. The table test passes both through the same caller contract.
go test -v ./factory-methodRun inside the extracted gof-min directory
Go adaptationgRPC 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.
// Builder creates a balancer.typeBuilderinterface{// Build creates a new balancer with the ClientConn.Build(ccClientConn,optsBuildOptions)Balancer// Name returns the name of balancers built by this builder.// It will be used to pick balancers (for example in service config).Name()string}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add PushCreator without modifying Deliver. Then replace Creator with func() Sender and compare the APIs.
go test -v ./factory-methodRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.
Light and dark themes each need matching buttons and checkboxes. Passing unrelated constructors can accidentally mix families.
The minimal 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.
Abstract factoryFactory
Product AButton
Product BCheckbox
Concrete familiesLight / Dark
Read the code in this order
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.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 28 lines
packageabstractfactorytypeButtoninterface{Draw()string}typeCheckboxinterface{Check()string}typeFactoryinterface{Button()ButtonCheckbox()Checkbox}typebuttonstringfunc(bbutton)Draw()string{returnstring(b)+":button"}typecheckboxstringfunc(ccheckbox)Check()string{returnstring(c)+":checked"}typeLightstruct{}func(Light)Button()Button{returnbutton("light")}func(Light)Checkbox()Checkbox{returncheckbox("light")}typeDarkstruct{}func(Dark)Button()Button{returnbutton("dark")}func(Dark)Checkbox()Checkbox{returncheckbox("dark")}// Screen obtains related products from the same family factory.funcScreen(fFactory)string{returnf.Button().Draw()+"/"+f.Checkbox().Check()}
go test -v ./abstract-factoryRun inside the extracted gof-min directory
Verify behavior, not just compilation
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.
go test -v ./abstract-factoryRun inside the extracted gof-min directory
Go adaptationdatabase/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.
// Conn is a connection to a database. It is not used concurrently// by multiple goroutines.//// Conn is assumed to be stateful.typeConninterface{// Prepare returns a prepared statement, bound to this connection.Prepare(querystring)(Stmt,error)// Close invalidates and potentially stops any current// prepared statements and transactions, marking this// connection as no longer in use.//// Because the sql package maintains a free pool of// connections and only calls Close when there's a surplus of// idle connections, it shouldn't be necessary for drivers to
func(mc*mysqlConn)Prepare(querystring)(driver.Stmt,error){ifmc.closed.Load(){returnnil,driver.ErrBadConn}// Send commanderr:=mc.writeCommandPacketStr(comStmtPrepare,query)iferr!=nil{// STMT_PREPARE is safe to retry. So we can return ErrBadConn here.mc.log(err)returnnil,driver.ErrBadConn}stmt:=&mysqlStmt{mc:mc,}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a HighContrast family, then intentionally mix a light button and dark checkbox in one factory.
go test -v ./abstract-factoryRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.
Assemble in steps; validate and hand off the product.
Start with the 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.
The minimal 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.
BuilderBuilder
ProductPlan
StepsNamed / Add
FinalizationBuild
Read the code in this order
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.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 21 lines
packagebuilderimport"errors"typePlanstruct{NamestringSteps[]string}typeBuilderstruct{namestringsteps[]string}func(b*Builder)Named(sstring)*Builder{b.name=s;returnb}func(b*Builder)Add(sstring)*Builder{b.steps=append(b.steps,s);returnb}func(b*Builder)Build()(Plan,error){ifb.name==""||len(b.steps)==0{returnPlan{},errors.New("name and steps required")}returnPlan{Name:b.name,Steps:append([]string(nil),b.steps...)},nil}
go test -v ./builderRun inside the extracted gof-min directory
Verify behavior, not just compilation
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.
go test -v ./builderRun inside the extracted gof-min directory
pattern_test.go
packagebuilderimport"testing"funcTestValidationAndBuiltValueOwnership(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()iferr!=nil{t.Fatal(err)}first.Steps[0]="changed by caller"second,err:=b.Add("write").Build()iferr!=nil{t.Fatal(err)}ifsecond.Steps[0]!="read"||len(second.Steps)!=2||len(first.Steps)!=1{t.Fatal(first,second)}}
Direct structureKubernetes 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.
// Verb sets the verb this request will use.func(r*Request)Verb(verbstring)*Request{r.verb=verbreturnr}// Prefix adds segments to the relative beginning to the request path. These// items will be placed before the optional Namespace, Resource, or Name sections.// Setting AbsPath will clear any previously set Prefix segmentsfunc(r*Request)Prefix(segments...string)*Request{ifr.err!=nil{returnr}r.pathPrefix=path.Join(r.pathPrefix,path.Join(segments...))returnr
func(r*Request)Name(resourceNamestring)*Request{ifr.err!=nil{returnr}iflen(resourceName)==0{r.err=fmt.Errorf("resource name may not be empty")returnr}iflen(r.resourceName)!=0{r.err=fmt.Errorf("resource name already set to %q, cannot change to %q",r.resourceName,resourceName)returnr}ifmsgs:=IsValidPathSegmentName(resourceName);len(msgs)!=0{r.err=fmt.Errorf("invalid resource name %q: %v",resourceName,msgs)returnr
// Error type:// - If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError// - http.Client.Do errors are returned directly.func(r*Request)Do(ctxcontext.Context)Result{logger:=klog.FromContext(ctx)ifr.body==nil{logBody(logger,2,"Request Body",r.bodyBytes)}varresultResulterr:=r.request(ctx,func(req*http.Request,resp*http.Response){result=r.transformResponse(ctx,resp,req)})iferr!=nil{returnResult{err:err,logger:logger}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Test reuse after Build, then decide whether Reset is needed.
go test -v ./builderRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Reset is optional. If added, it must not affect returned plans. A shallow slice copy is insufficient once products contain mutable nested pointers.
Turn the change into a test and explain the pattern’s applicability boundary.
Clone an existing object with an explicit copy depth.
Start with the problem
Derive documents from an existing template without letting edits corrupt the original. Struct assignment does not deep-copy slices or maps.
The minimal 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.
PrototypeDocument
Clone operationClone
Independent stateTags / Labels
Read the code in this order
The test mutates the clone’s slice and map, checks the original, and preserves nil-collection semantics.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 19 lines
packageprototypeimport("maps""slices")typeDocumentstruct{NamestringTags[]stringLabelsmap[string]string}// Clone preserves nil collections and copies each mutable collection here.func(dDocument)Clone()Document{d.Tags=slices.Clone(d.Tags)d.Labels=maps.Clone(d.Labels)returnd}
go test -v ./prototypeRun inside the extracted gof-min directory
Verify behavior, not just compilation
The test mutates the clone’s slice and map, checks the original, and preserves nil-collection semantics.
go test -v ./prototypeRun inside the extracted gof-min directory
Direct structureKubernetes 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.
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.func(in*Pod)DeepCopyInto(out*Pod){*out=*inout.TypeMeta=in.TypeMetain.ObjectMeta.DeepCopyInto(&out.ObjectMeta)in.Spec.DeepCopyInto(&out.Spec)in.Status.DeepCopyInto(&out.Status)return}// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pod.func(in*Pod)DeepCopy()*Pod{ifin==nil{returnnil}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Change Labels to map[string][]string and expose the new aliasing bug.
go test -v ./prototypeRun inside the extracted gof-min directory
Think first, then reveal the reasoning
maps.Clone copies entries, but nested slices still share arrays. Clone each value slice and add a nested-mutation test.
Turn the change into a test and explain the pattern’s applicability boundary.
Callers need one read-only default configuration and may access it concurrently for the first time. An unsynchronized nil check races.
The minimal 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.
Access pointDefault
Shared instanceSettings
One-time initializationsync.OnceValue
Read the code in this order
The OnceValue wrapper itself must be created once. Constructing it inside Default would create a fresh initialization scope on every call.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 12 lines
packagesingletonimport"sync"// Settings exposes no mutation API; all callers share one package default.typeSettingsstruct{endpointstring}func(s*Settings)Endpoint()string{returns.endpoint}varinstance=sync.OnceValue(func()*Settings{return&Settings{endpoint:"localhost:8080"}})funcDefault()*Settings{returninstance()}
go test -v ./singletonRun inside the extracted gof-min directory
Verify behavior, not just compilation
The OnceValue wrapper itself must be created once. Constructing it inside Default would create a fresh initialization scope on every call.
go test -v ./singletonRun inside the extracted gof-min directory
Related mechanismtime.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.
// Local represents the system's local time zone.// On Unix systems, Local consults the TZ environment// variable to find the time zone to use. No TZ means// use the system default /etc/localtime.// TZ="" means use UTC.// TZ="foo" means use file foo in the system timezone directory.varLocal*Location=&localLoc// localLoc is separate so that initLocal can initialize// it even if a client has changed Local.varlocalLocLocationvarlocalOncesync.Oncefunc(l*Location)get()*Location{ifl==nil{
// returned by f. The returned function may be called concurrently.//// If f panics, the returned function will panic with the same value on every call.funcOnceValue[Tany](ffunc()T)func()T{// Use a struct so that there's a single heap allocation.d:=struct{ffunc()TonceOncevalidboolpanyresultT}{f:f,}returnfunc()T{
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Move OnceValue inside Default and observe identity failure; then consider a mutable map in Settings.
go test -v ./singletonRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Once protects initialization, not subsequent mutation. Shared mutable state still requires synchronization or immutable snapshots.
Turn the change into a test and explain the pattern’s applicability boundary.
Direct structurenet/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.
// ordinary functions as HTTP handlers. If f is a function// with the appropriate signature, HandlerFunc(f) is a// [Handler] that calls f.typeHandlerFuncfunc(ResponseWriter,*Request)// ServeHTTP calls f(w, r).func(fHandlerFunc)ServeHTTP(wResponseWriter,r*Request){f(w,r)}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a KelvinSensor adapter without changing IsBoiling.
go test -v ./adapterRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Keep conversion in the adapter and test reference points. If sources can fail, define the target error contract before adapting them.
Turn the change into a test and explain the pattern’s applicability boundary.
Normal and quiet remotes must both support TVs and radios. One type per combination multiplies the hierarchy.
The minimal 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.
AbstractionRemote
Refined abstractionQuietRemote
ImplementorDevice
ImplementationsTV / Radio
Read the code in this order
Read primitive Device operations, then how On/Sleep compose them. A new device should not require a new family of remote types.
Complete file, including package and imports. Identical to the ZIP source.
Go adaptationZap 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.
typeLoggerstruct{corezapcore.CoredevelopmentbooladdCallerboolonPaniczapcore.CheckWriteHook// default is WriteThenPaniconFatalzapcore.CheckWriteHook// default is WriteThenFatalnamestringerrorOutputzapcore.WriteSynceraddStackzapcore.LevelEnablercallerSkipint
// Core is a minimal, fast logger interface. It's designed for library authors// to wrap in a more user-friendly API.typeCoreinterface{LevelEnabler// With adds structured context to the Core.With([]Field)Core// Check determines whether the supplied Entry should be logged (using the// embedded LevelEnabler and possibly some extra logic). If the entry// should be logged, the Core adds itself to the CheckedEntry and returns// the result.//// Callers must use Check before calling Write.Check(Entry,*CheckedEntry)*CheckedEntry// Write serializes the Entry and any Fields supplied at the log site and
func(log*Logger)check(lvlzapcore.Level,msgstring)*zapcore.CheckedEntry{// Logger.check must always be called directly by a method in the// Logger interface (e.g., Check, Info, Fatal).// This skips Logger.check and the Info/Fatal/Check/etc. method that// called it.constcallerSkipOffset=2// Check the level first to reduce the cost of disabled log calls.// Since Panic and higher may exit, we skip the optimization for those levels.iflvl<zapcore.DPanicLevel&&!log.core.Enabled(lvl){returnnil}// Create basic checked entry thru the core; this will be non-nil if the// log message will actually be written somewhere.
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a Speaker device and an AlarmRemote; count the added types.
go test -v ./bridgeRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.
To calculate directory size, callers should not classify each child as a file or another directory.
The minimal design
File and Directory implement Size. Directory holds []Node and recursively sums children. Callers invoke root.Size; an empty directory contributes zero.
ComponentNode
LeafFile
CompositeDirectory
Read the code in this order
The test combines leaves, nested directories, and an empty directory to verify uniform traversal and aggregation.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 17 lines
packagecomposite// Node represents a finite, acyclic tree. Cycles are outside this contract.typeNodeinterface{Size()int}typeFileintfunc(fFile)Size()int{returnint(f)}typeDirectory[]Nodefunc(dDirectory)Size()int{total:=0for_,child:=ranged{total+=child.Size()}returntotal}
go test -v ./compositeRun inside the extracted gof-min directory
Verify behavior, not just compilation
The test combines leaves, nested directories, and an empty directory to verify uniform traversal and aggregation.
go test -v ./compositeRun inside the extracted gof-min directory
Direct structureerrors.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.
// Join returns an error that wraps the given errors.// Any nil error values are discarded.// Join returns nil if every value in errs is nil.// The error formats as the concatenation of the strings obtained// by calling the Error method of each element of errs, with a newline// between each string.//// A non-nil error returned by Join implements the Unwrap() []error method.// The errors may be inspected with [Is] and [As].funcJoin(errs...error)error{n:=0for_,err:=rangeerrs{iferr!=nil{n++}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add Count to Node. Which types must change?
go test -v ./compositeRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Every leaf and composite must implement it. With stable node kinds and many operations, compare Visitor, which moves that extension cost to visitors.
Turn the change into a test and explain the pattern’s applicability boundary.
Count bytes from any Reader without editing file, buffer, or network implementations.
The minimal 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.
Componentio.Reader
Concrete componentSource
DecoratorCountingReader
Read the code in this order
Study the test returning n>0 and io.EOF together. Checking the error before counting would lose the final bytes.
Complete file, including package and imports. Identical to the ZIP source.
go test -v ./decoratorRun inside the extracted gof-min directory
Verify behavior, not just compilation
Study the test returning n>0 and io.EOF together. Checking the error before counting would lose the final bytes.
go test -v ./decoratorRun inside the extracted gof-min directory
pattern_test.go
packagedecoratorimport("io""strings""testing")typefinalChunkstruct{}func(finalChunk)Read(p[]byte)(int,error){returncopy(p,"end"),io.EOF}funcTestPreservesBytesAndTerminalError(t*testing.T){reader:=&CountingReader{Source:strings.NewReader("hello")}got,err:=io.ReadAll(reader)iferr!=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))ifn!=3||err!=io.EOF||reader.Bytes!=3{t.Fatal("lost bytes returned with EOF")}}
Direct structureio.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.
// LimitReader returns a Reader that reads from r// but stops with EOF after n bytes.// The underlying implementation is a *LimitedReader.funcLimitReader(rReader,nint64)Reader{return&LimitedReader{r,n}}// A LimitedReader reads from R but limits the amount of// data returned to just N bytes. Each call to Read// updates N to reflect the new amount remaining.// Read returns EOF when N <= 0 or when the underlying R returns EOF.typeLimitedReaderstruct{RReader// underlying readerNint64// max bytes remaining}func(l*LimitedReader)Read(p[]byte)(nint,errerror){
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Nest two CountingReaders and verify both count the same bytes.
go test -v ./decoratorRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Each layer observes its actual Read result. Short reads and errors must still count delivered bytes, never len(p).
Turn the change into a test and explain the pattern’s applicability boundary.
Expose a caller-oriented entry point over subsystems.
Start with the problem
Every caller repeats Get→Decode and error wrapping, and must understand subsystem sequencing.
The minimal design
Service.Load orchestrates Store and Decoder and returns Config. Subsystems need not share an interface, and the facade does not impersonate either subsystem.
FacadeService.Load
Subsystem AStore
Subsystem BDecoder
Read the code in this order
The test proves decoding is skipped after a load failure and errors.Is still sees the cause; success verifies the orchestration result.
Complete file, including package and imports. Identical to the ZIP source.
go test -v ./facadeRun inside the extracted gof-min directory
Verify behavior, not just compilation
The test proves decoding is skipped after a load failure and errors.Is still sees the cause; success verifies the orchestration result.
go test -v ./facadeRun inside the extracted gof-min directory
pattern_test.go
packagefacadeimport("errors""testing")typestoreFuncfunc(string)(string,error)func(fstoreFunc)Get(kstring)(string,error){returnf(k)}typedecodeFuncfunc(string)(Config,error)func(fdecodeFunc)Decode(sstring)(Config,error){returnf(s)}funcTestFacadeOrchestratesAndPreservesError(t*testing.T){missing:=errors.New("missing")decoded:=falses:=Service{Store:storeFunc(func(string)(string,error){return"",missing}),Decoder:decodeFunc(func(rawstring)(Config,error){decoded=true;returnConfig{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})ifgot,err:=s.Load("key");err!=nil||got.Address!="localhost"{t.Fatal(got,err)}}
Go adaptationhttp.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.
funcGet(urlstring)(resp*Response,errerror){returnDefaultClient.Get(url)}// Get issues a GET to the specified URL. If the response is one of the// following redirect codes, Get follows the redirect after calling the// [Client.CheckRedirect] function://// 301 (Moved Permanently)// 302 (Found)// 303 (See Other)// 307 (Temporary Redirect)// 308 (Permanent Redirect)//// An error is returned if the [Client.CheckRedirect] function fails
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Return a sentinel error from Decoder and check whether the caller can identify it.
go test -v ./facadeRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Wrap with %w and identify the decode stage. Avoid string matching and do not erase every error identity merely to hide a subsystem.
Turn the change into a test and explain the pattern’s applicability boundary.
Many glyphs use the same font; duplicating expensive style data for each wastes memory.
The minimal 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.
FlyweightStyle
FactoryPool.Get
Intrinsic statefont
Extrinsic stateGlyph.Text / X
Read the code in this order
Compare style pointers, then glyph Text/X values. The intrinsic object is shared, not the entire usage context.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 32 lines
packageflyweightimport"sync"typeStylestruct{fontstring}func(s*Style)Font()string{returns.font}// Pool keeps strong references forever; a real unbounded key space needs a policy.typePoolstruct{musync.Mutexstylesmap[string]*Style}func(p*Pool)Get(fontstring)*Style{p.mu.Lock()deferp.mu.Unlock()ifp.styles==nil{p.styles=make(map[string]*Style)}ifp.styles[font]==nil{p.styles[font]=&Style{font:font}}returnp.styles[font]}// Extrinsic data stays outside the shared immutable Style.typeGlyphstruct{Style*StyleTextstringXint}
go test -v ./flyweightRun inside the extracted gof-min directory
Verify behavior, not just compilation
Compare style pointers, then glyph Text/X values. The intrinsic object is shared, not the entire usage context.
go test -v ./flyweightRun inside the extracted gof-min directory
Direct structureunique.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.
// Handle is a globally unique identity for some value of type T.//// Two handles compare equal exactly if the two values used to create the handles// would have also compared equal. The comparison of two handles is trivial and// typically much more efficient than comparing the values used to create them.typeHandle[Tcomparable]struct{value*T}// Value returns a shallow copy of the T value that produced the Handle.// Value is safe for concurrent use by multiple goroutines.func(hHandle[T])Value()T{return*h.value}
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Would adding an eviction limit break the same-key/same-pointer promise?
go test -v ./flyweightRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Yes: a caller can retain the old Style while eviction permits another instance. Define the identity scope before choosing caching, interning, or weak references.
Turn the change into a test and explain the pattern’s applicability boundary.
Control access or forwarding through the same contract.
Start with the problem
Callers need Store.Read, but restricted keys must never reach the backend. Repeating checks at each call site is fragile.
The minimal 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.
SubjectStore
Real subjectNext
ProxyGuard
Read the code in this order
The second assertion returns a backend error, ensuring permission does not get confused with successful reading.
Complete file, including package and imports. Identical to the ZIP source.
Direct structurehttputil.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.
func(p*ReverseProxy)ServeHTTP(rwhttp.ResponseWriter,req*http.Request){transport:=p.Transportiftransport==nil{transport=http.DefaultTransport}ctx:=req.Context()ifctx.Done()!=nil{// CloseNotifier predates context.Context, and has been// entirely superseded by it. If the request contains// a Context that carries a cancellation signal, don't// bother spinning up a goroutine to watch the CloseNotify// channel (if any).//// If the request Context has a nil Done channel (which
Pass a request along a chain until a handler accepts it.
Start with the problem
Several handlers might accept a path. The caller should not choose one, and not every handler should perform a side effect.
The minimal 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.
RequestRequest
HandlersHandler functions
Chain ownerDispatch
Read the code in this order
The test places a skipped handler before an accepting one and an unreachable one after it. Two calls prove short-circuiting.
Complete file, including package and imports. Identical to the ZIP source.
Go adaptationGin 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.
// Next should be used only inside middleware.// It executes the pending handlers in the chain inside the calling handler.// See example in GitHub.func(c*Context)Next(){c.index++forc.index<safeInt8(len(c.handlers)){ifc.handlers[c.index]!=nil{c.handlers[c.index](c)}c.index++}}// IsAborted returns true if the current context was aborted.func(c*Context)IsAborted()bool{
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a fallback handler and move it before the others.
go test -v ./chain-of-responsibilityRun inside the extracted gof-min directory
Think first, then reveal the reasoning
An always-accepting fallback must come last or later handlers become unreachable. Preserve the intended order with a behavior test.
Turn the change into a test and explain the pattern’s applicability boundary.
Represent an action as a value for deferred execution.
Start with the problem
A UI, CLI, or job queue wants to assemble actions for later invocation instead of editing the document immediately.
The minimal 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.
CommandCommand
Concrete commandReplace
ReceiverDocument
InvokerRun
Read the code in this order
The test verifies construction leaves the document unchanged, execution updates it, and a failed command prevents later actions.
Complete file, including package and imports. Identical to the ZIP source.
Direct structureCobra 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.
Represent and evaluate a small language as an expression tree.
Start with the problem
Authorization rules need variables, AND, and NOT without hard-coding another conditional for every rule.
The minimal 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.
ExpressionExpr
TerminalsVar / Literal
NonterminalsAnd / Not
ContextEnv
Read the code in this order
The rule is admin AND NOT blocked. Tests cover truth, an unknown variable, and short-circuiting false AND missing.
Complete file, including package and imports. Identical to the ZIP source.
go test -v ./interpreterRun inside the extracted gof-min directory
Verify behavior, not just compilation
The rule is admin AND NOT blocked. Tests cover truth, an unknown variable, and short-circuiting false AND missing.
go test -v ./interpreterRun inside the extracted gof-min directory
pattern_test.go
packageinterpreterimport"testing"funcTestExpressionSemanticsAndShortCircuit(t*testing.T){rule:=And{Left:Var("admin"),Right:Not{Inner:Var("blocked")}}ifgot,err:=rule.Eval(Env{"admin":true,"blocked":false});err!=nil||!got{t.Fatal(got,err)}ifgot,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")}}
Go adaptationtext/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.
// generating output as they go.func(s*state)walk(dotreflect.Value,nodeparse.Node){s.at(node)switchnode:=node.(type){case*parse.ActionNode:// Do not pop variables so they persist until next end.// Also, if the action declares variables, don't print the result.val:=s.evalPipeline(dot,node.Pipe)iflen(node.Pipe.Decl)==0{s.printValue(node,val)}case*parse.BreakNode:panic(walkBreak)case*parse.CommentNode:case*parse.ContinueNode:
// A Node is an element in the parse tree. The interface is trivial.// The interface contains an unexported method so that only// types local to this package can satisfy it.typeNodeinterface{Type()NodeTypeString()string// Copy does a deep copy of the Node and all its components.// To avoid type assertions, some XxxNodes also have specialized// CopyXxx methods that return *XxxNode.Copy()NodePosition()Pos// byte position of start of node in full original input string// tree returns the containing *Tree.// It is unexported so all implementations of Node are in this package.tree()*Tree// writeTo writes the String output to the builder.
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add Or and verify true OR missing skips the right side.
go test -v ./interpreterRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Short-circuiting is language semantics. Evaluating both operands first changes error and side-effect behavior.
Turn the change into a test and explain the pattern’s applicability boundary.
Callers need ordered traversal without knowing whether storage is a slice, tree, or pages.
The minimal 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.
AggregateCollection
Iteratoriter.Seq[string]
Consumeryield / range
Read the code in this order
The test stops on the first yield, then collects a full traversal, showing that early exit does not exhaust future traversals.
Complete file, including package and imports. Identical to the ZIP source.
Go adaptationgoogle/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.
// AscendRange calls the iterator for every value in the tree within the range// [greaterOrEqual, lessThan), until iterator returns false.func(t*BTree)AscendRange(greaterOrEqual,lessThanItem,iteratorItemIterator){ift.root==nil{return}t.root.iterate(ascend,greaterOrEqual,lessThan,true,false,iterator)}// AscendLessThan calls the iterator for every value in the tree within the range// [first, pivot), until iterator returns false.func(t*BTree)AscendLessThan(pivotItem,iteratorItemIterator){ift.root==nil{return}
// Preorder returns an iterator over all the nodes of the syntax tree// beneath (and including) the specified root, in depth-first// preorder.//// For greater control over the traversal of each subtree, use// [Inspect] or [PreorderStack].funcPreorder(rootNode)iter.Seq[Node]{returnfunc(yieldfunc(Node)bool){ok:=trueInspect(root,func(nNode)bool{ifn!=nil{// yield must not be called once ok is false.ok=ok&&yield(n)}returnok
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a Filter sequence that propagates a downstream break to the original producer.
go test -v ./iteratorRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Yield only matching elements and stop upstream immediately when downstream returns false. Check producer call counts, not only the resulting values.
Turn the change into a test and explain the pattern’s applicability boundary.
Members exchange messages; direct references among all of them scatter relationships and routing policy.
The minimal design
Members know only Room. Send delegates intent; route selects the recipient and delivers. Join rejects duplicate names, and Messages copies the inbox.
MediatorRoom
ColleaguesMember
Coordinationroute
Read the code in this order
Member a sends to b without holding b’s pointer. Tests cover unknown recipients, duplicate identity, and inbox ownership.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 36 lines
packagemediatorimport("fmt""slices")// Room and Members have a single owner; concurrent use needs a separate policy.typeRoomstruct{membersmap[string]*Member}typeMemberstruct{room*Roomnamestringinbox[]string}func(r*Room)Join(namestring)(*Member,error){ifr.members==nil{r.members=make(map[string]*Member)}if_,exists:=r.members[name];exists{returnnil,fmt.Errorf("duplicate member %s",name)}m:=&Member{room:r,name:name}r.members[name]=mreturnm,nil}func(m*Member)Send(to,textstring)error{returnm.room.route(m.name,to,text)}func(m*Member)Messages()[]string{returnslices.Clone(m.inbox)}func(r*Room)route(from,to,textstring)error{recipient,ok:=r.members[to]if!ok{returnfmt.Errorf("unknown recipient %s",to)}recipient.inbox=append(recipient.inbox,from+":"+text)returnnil}
go test -v ./mediatorRun inside the extracted gof-min directory
Verify behavior, not just compilation
Member a sends to b without holding b’s pointer. Tests cover unknown recipients, duplicate identity, and inbox ownership.
go test -v ./mediatorRun inside the extracted gof-min directory
Related mechanismfzf 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.
// Events is a type that associates EventType to any datatypeEventsmap[EventType]any// EventBox is used for coordinating eventstypeEventBoxstruct{eventsEventscond*sync.Condignoremap[EventType]bool}// NewEventBox returns a new EventBoxfuncNewEventBox()*EventBox{return&EventBox{events:make(Events),cond:sync.NewCond(&sync.Mutex{}),
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a rule allowing a member to send only within its group.
go test -v ./mediatorRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.
Save restorable state without exposing its representation.
Start with the problem
An editor needs undo, while its history manager should not inspect or mutate internal fields.
The minimal design
Save returns a Snapshot with unexported fields. Editor restores itself and rejects foreign snapshots. The caretaker stores the token without interpreting it.
OriginatorEditor
MementoSnapshot
Caretakercaller / history
Read the code in this order
The test saves, edits, restores, reuses the snapshot, and rejects a different owner.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 20 lines
packagemementoimport"errors"typeEditorstruct{textstring}typeSnapshotstruct{owner*Editortextstring}func(e*Editor)Set(sstring){e.text=s}func(e*Editor)Text()string{returne.text}func(e*Editor)Save()Snapshot{returnSnapshot{owner:e,text:e.text}}func(e*Editor)Restore(sSnapshot)error{ifs.owner!=e{returnerrors.New("snapshot belongs to another editor")}e.text=s.textreturnnil}
go test -v ./mementoRun inside the extracted gof-min directory
Verify behavior, not just compilation
The test saves, edits, restores, reuses the snapshot, and rejects a different owner.
go test -v ./mementoRun inside the extracted gof-min directory
Go adaptationgo-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.
// Snapshot returns an identifier for the current revision of the state.func(s*StateDB)Snapshot()int{returns.journal.snapshot()}// RevertToSnapshot reverts all state changes made since the given revision.func(s*StateDB)RevertToSnapshot(revidint){s.journal.revertToSnapshot(revid,s)}
// snapshot returns an identifier for the current revision of the state.func(j*journal)snapshot()int{id:=j.nextRevisionIdj.nextRevisionId++j.validRevisions=append(j.validRevisions,revision{id,j.length()})returnid}// revertToSnapshot reverts all state changes made since the given revision.func(j*journal)revertToSnapshot(revidint,s*StateDB){// Find the snapshot in the stack of valid snapshots.idx:=sort.Search(len(j.validRevisions),func(iint)bool{returnj.validRevisions[i].id>=revid})ifidx==len(j.validRevisions)||j.validRevisions[idx].id!=revid{
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add two undo levels and decide whether restoring an older snapshot preserves newer history.
go test -v ./mementoRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Choose explicit history semantics: branching, truncation, or redo. Do not generalize the lab’s immutable-token behavior to journal systems.
Turn the change into a test and explain the pattern’s applicability boundary.
Notify a set of subscribers from one event source.
Start with the problem
Independent consumers need state-change notifications without hard-coding their types in the subject.
The minimal 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.
SubjectTopic
Observerscallback functions
RegistrationSubscribe
NotificationPublish
Read the code in this order
Self-unsubscription receives only the first event. Parallel publication uses an atomic counter because callbacks themselves can run concurrently.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 35 lines
packageobserverimport"sync"typeTopicstruct{musync.Mutexnextintlistenersmap[int]func(string)}func(t*Topic)Subscribe(fnfunc(string))func(){t.mu.Lock()defert.mu.Unlock()ift.listeners==nil{t.listeners=make(map[int]func(string))}id:=t.nextt.next++t.listeners[id]=fnreturnfunc(){t.mu.Lock();defert.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(valuestring){t.mu.Lock()snapshot:=make([]func(string),0,len(t.listeners))for_,fn:=ranget.listeners{snapshot=append(snapshot,fn)}t.mu.Unlock()for_,fn:=rangesnapshot{fn(value)}}
go test -v ./observerRun inside the extracted gof-min directory
Verify behavior, not just compilation
Self-unsubscription receives only the first event. Parallel publication uses an atomic counter because callbacks themselves can run concurrently.
go test -v ./observerRun inside the extracted gof-min directory
Direct structureKubernetes 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.
func(p*sharedProcessor)addListener(listener*processorListener)(ResourceEventHandlerRegistration,bool){p.listenersLock.Lock()deferp.listenersLock.Unlock()ifp.listeners==nil{p.listeners=make(map[*processorListener]bool)}p.listeners[listener]=trueifp.listenersStarted{// Not starting listener.watchSynced!// The caller must first add the initial list, then start it.p.wg.Start(listener.run)p.wg.Start(listener.pop)
func(p*sharedProcessor)distribute(objinterface{},syncbool){p.listenersLock.RLock()deferp.listenersLock.RUnlock()// Before we start blocking on writes to the listeners' channels,// ensure that they all have been started. If the processor stops,// p.listeners gets cleared, in which case we also continue here// and return without doing anything.for!p.listenersStarted&&len(p.listeners)>0{p.listenersRCond.Wait()}forlistener,isSyncing:=rangep.listeners{switch{case!sync:
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Register a new observer during notification. Should it receive the current event?
go test -v ./observerRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.
Let internal state change the behavior of the same action.
Start with the problem
A gate rejects Pass while locked and allows it while unlocked, then relocks. A large switch in every action duplicates transitions.
The minimal design
Gate delegates Coin/Pass to its current mode. States decide results and transitions. The client does not select an algorithm explicitly.
ContextGate
State interfacemode
Concrete stateslocked / unlocked
Read the code in this order
The test checks rejection, coin insertion, one pass, then rejection again. A second coin does not accumulate another pass in this policy.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 25 lines
packagestateimport"errors"varErrLocked=errors.New("gate is locked")typemodeinterface{coin(*Gate)pass(*Gate)error}typeGatestruct{currentmode}funcNew()*Gate{return&Gate{current:locked{}}}func(g*Gate)Coin(){g.current.coin(g)}func(g*Gate)Pass()error{returng.current.pass(g)}typelockedstruct{}func(locked)coin(g*Gate){g.current=unlocked{}}func(locked)pass(*Gate)error{returnErrLocked}typeunlockedstruct{}func(unlocked)coin(*Gate){}func(unlocked)pass(g*Gate)error{g.current=locked{};returnnil}
go test -v ./stateRun inside the extracted gof-min directory
Verify behavior, not just compilation
The test checks rejection, coin insertion, one pass, then rejection again. A second coin does not accumulate another pass in this policy.
go test -v ./stateRun inside the extracted gof-min directory
pattern_test.go
packagestateimport("errors""testing")funcTestBehaviorChangesWithInternalState(t*testing.T){g:=New()iferr:=g.Pass();!errors.Is(err,ErrLocked){t.Fatal(err)}g.Coin()g.Coin()iferr:=g.Pass();err!=nil{t.Fatal(err)}iferr:=g.Pass();!errors.Is(err,ErrLocked){t.Fatal("did not return to locked state")}}
Go adaptationtext/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.
// stateFn represents the state of the scanner as a function that returns the next state.typestateFnfunc(*lexer)stateFn// lexer holds the state of the scanner.typelexerstruct{namestring// the name of the input; used only for error reportsinputstring// the string being scannedleftDelimstring// start of action markerrightDelimstring// end of action markerposPos// current position in the inputstartPos// start position of this itematEOFbool// we have hit the end of input and returned eofparenDepthint// nesting depth of ( ) exprslineint// 1+number of newlines seenstartLineint// start line of this item
// nextItem returns the next item from the input.// Called by the parser, not in the lexing goroutine.func(l*lexer)nextItem()item{l.item=item{itemEOF,l.pos,"EOF",l.startLine}state:=lexTextifl.insideAction{state=lexInsideAction}for{state=state(l)ifstate==nil{returnl.item}}}
funclexText(l*lexer)stateFn{ifx:=strings.Index(l.input[l.pos:],l.leftDelim);x>=0{ifx>0{l.pos+=Pos(x)// Do we trim any trailing space?trimLength:=Pos(0)delimEnd:=l.pos+Pos(len(l.leftDelim))ifhasLeftTrimMarker(l.input[delimEnd:]){trimLength=rightTrimLength(l.input[l.start:l.pos])}l.pos-=trimLengthl.line+=strings.Count(l.input[l.start:l.pos],"\n")i:=l.thisItem(itemText)l.pos+=trimLengthl.ignore()
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add a maintenance state and specify who can enter or leave it.
go test -v ./stateRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Transition authorization belongs in the context API, not a publicly mutable state field. Serialize actions at the context boundary if used concurrently.
Turn the change into a test and explain the pattern’s applicability boundary.
Keep the workflow; replace an algorithmic decision.
Start with the problem
The same values need ascending or descending order. Duplicating the sorting implementation creates unnecessary maintenance.
The minimal 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.
The test uses one Sorted implementation with two comparators, duplicate values, and an input-ownership check.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 20 lines
packagestrategyimport"slices"typeLessfunc(a,bint)bool// Sorted accepts a strict weak ordering and leaves the input unchanged.funcSorted(input[]int,lessLess)[]int{result:=slices.Clone(input)slices.SortFunc(result,func(a,bint)int{ifless(a,b){return-1}ifless(b,a){return1}return0})returnresult}
go test -v ./strategyRun inside the extracted gof-min directory
Verify behavior, not just compilation
The test uses one Sorted implementation with two comparators, duplicate values, and an input-ownership check.
go test -v ./strategyRun inside the extracted gof-min directory
Direct structureBackOff 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.
// BackOff is a backoff policy for retrying an operation.typeBackOffinterface{// NextBackOff returns the duration to wait before retrying the operation,// backoff.Stop to indicate that no more retries should be made.//// Example usage://// duration := backoff.NextBackOff()// if duration == backoff.Stop {// // Do not retry operation.// } else {// // Sleep for duration and retry operation.// }//NextBackOff()time.Duration
// Stop retrying if context is cancelled.ifcerr:=context.Cause(ctx);cerr!=nil{returnres,&RetryError{LastErr:lastErr,Cause:cerr}}// Calculate next backoff duration.next:=args.BackOff.NextBackOff()ifnext==Stop{returnres,&RetryError{LastErr:lastErr,Cause:ErrExhausted}}// Reset backoff if a RetryAfterError requested a specific delay.ifretryAfter!=nil{next=retryAfter.Durationargs.BackOff.Reset()
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Sort by absolute value with an explicit tie-breaker.
go test -v ./strategyRun inside the extracted gof-min directory
Think first, then reveal the reasoning
Compare magnitude, then original value consistently. Negating the minimum int overflows, so use a safe magnitude representation or restrict the input domain.
Turn the change into a test and explain the pattern’s applicability boundary.
Fix the algorithm skeleton; expose selected hooks.
Start with the problem
Import workflows share Load→Transform→Save but vary each step, while ordering and failure handling must stay consistent.
The minimal 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.
TemplateRun
Primitive operationsSteps
HooksLoad / Transform / Save
Read the code in this order
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.
Complete file, including package and imports. Identical to the ZIP source.
pattern.go · 34 lines
packagetemplatemethodimport("context""fmt")typeStepsinterface{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.funcRun(ctxcontext.Context,sSteps)error{iferr:=ctx.Err();err!=nil{returnerr}raw,err:=s.Load(ctx)iferr!=nil{returnfmt.Errorf("load: %w",err)}value,err:=s.Transform(raw)iferr!=nil{returnfmt.Errorf("transform: %w",err)}iferr:=ctx.Err();err!=nil{returnerr}iferr:=s.Save(ctx,value);err!=nil{returnfmt.Errorf("save: %w",err)}returnnil}
go test -v ./template-methodRun inside the extracted gof-min directory
Verify behavior, not just compilation
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.
go test -v ./template-methodRun inside the extracted gof-min directory
Go adaptationBackoff 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.
funcRetry[Tany](ctxcontext.Context,operationOperation[T],opts...RetryOption)(T,error){// Initialize default retry options.args:=&retryOptions{BackOff:NewExponentialBackOff(),Timer:&defaultTimer{},MaxElapsedTime:DefaultMaxElapsedTime,}// Apply user-provided options to the default settings.for_,opt:=rangeopts{opt(args)}deferargs.Timer.Stop()
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Make Transform cancellation-aware and prevent Save after cancellation.
go test -v ./template-methodRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.
Add operations externally when node kinds are stable.
Start with the problem
The same AST needs evaluation and counting. Continually adding methods makes nodes own too many operations.
The minimal 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.
ElementNode
Concrete elementsNumber / Add
VisitorVisitor
OperationsEval / Count
Read the code in this order
One tree evaluates to 6 and contains 5 nodes; evaluation remains unchanged afterward. Compare Interpreter, where evaluation lives on expressions instead of visitors.
Complete file, including package and imports. Identical to the ZIP source.
go test -v ./visitorRun inside the extracted gof-min directory
Verify behavior, not just compilation
One tree evaluates to 6 and contains 5 nodes; evaluation remains unchanged afterward. Compare Interpreter, where evaluation lives on expressions instead of visitors.
go test -v ./visitorRun inside the extracted gof-min directory
Go adaptationgo/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.
// A Visitor's Visit method is invoked for each node encountered by [Walk].// If the result visitor w is not nil, [Walk] visits each of the children// of node with the visitor w, followed by a call of w.Visit(nil).typeVisitorinterface{Visit(nodeNode)(wVisitor)}funcwalkList[NNode](vVisitor,list[]N){for_,node:=rangelist{Walk(v,node)}}// TODO(gri): Investigate if providing a closure to Walk leads to// simpler use (and may help eliminate Inspect in turn).
default:panic(fmt.Sprintf("ast.Walk: unexpected node type %T",n))}v.Visit(nil)}typeinspectorfunc(Node)boolfunc(finspector)Visit(nodeNode)Visitor{iff(node){returnf}returnnil
Source links pin full commits. Stars / archived are metadata snapshots from 2026-09-06, not quality scores or authors’ GoF claims.
Make one change yourself
Add expression printing, then add Multiply and compare the change surface.
go test -v ./visitorRun inside the extracted gof-min directory
Think first, then reveal the reasoning
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.
Turn the change into a test and explain the pattern’s applicability boundary.