Hugo — Patterns#
Concurrency patterns#
Worker Pool via common/para#
- Usage: General-purpose bounded parallel execution. Used when a batch of independent tasks must be processed with a capped concurrency level.
- Example:
common/para/para.go—Workers.semis a buffered channel acting as a semaphore.Run(fn)blocks until a slot is free, then launches the task in a goroutine.errgroup.Groupcollects errors. - Assessment: Clean, idiomatic. The semaphore-over-channel approach is the standard Go idiom for bounded goroutine pools. The interface
Runner(withRunandWait) is well-abstracted.
Generic Typed Worker Pool via common/rungroup#
- Usage: A more recent (2024) alternative to
para. Creates N goroutines that drain a typed channelchan T. The caller enqueues work items; workers call a user-suppliedHandle(ctx, T) errorfunc. - Example:
common/rungroup/rungroup.go—Run[T](ctx, Config[T]) Group[T]. Uses Go generics for full type safety; nointerface{}boxing. - Assessment: Excellent use of generics. The
Group[T]interface (Enqueue(T) error,Wait() error) is minimal and composable. The pattern is a textbook typed fan-out. Preferred overparafor new code.
errgroup for Structured Parallel Work#
- Usage: Concurrent phases with first-error semantics. Found in: WASM worker pool startup (
internal/warpc), parallel page assembly (hugolib/content_map_page_assembler.go), server command lifecycle (commands/server.go,commands/hugobuilder.go). - Example:
commands/server.go:946—wg1, ctx := errgroup.WithContext(context.Background())launches HTTP listener and WebSocket server as goroutines; context cancel propagates shutdown. - Assessment: Standard and correct usage. Hugo uses
errgroupexclusively for structured concurrency (no rawgo funcfor error-collecting work).
Event Debouncing via Batcher#
- Usage:
watcher/batcher.goaccumulatesfsnotify.Eventitems until a ticker fires, then emits the full batch. Prevents per-file rebuild thrashing during rapid saves. - Example:
watcher/batcher.go:52-66—run()goroutine selects onFileWatcher.Events(),ticker.C, anddone. On tick: if events accumulated, send batch; reset slice. - Assessment: Idiomatic ticker+select debounce. The buffered
Events chan []fsnotify.Eventprevents back-pressure from the consumer. A well-worn pattern, cleanly packaged.
Semaphore via semaphore.Weighted#
- Usage:
commands/hugobuilder.go:70—fullRebuildSem *semaphore.Weighted(weight=1) ensures at most one full rebuild runs concurrently, even if multiple file-change events arrive simultaneously. - Example:
commands/server.go:103—fullRebuildSem: semaphore.NewWeighted(1). - Assessment: Correct use of
golang.org/x/sync/semaphorefor mutual exclusion between long-running operations. Preferable to async.Mutexhere becauseTryAcquireallows skipping queued rebuilds.
Observer/Listener Pattern with Self-Removing Generics#
- Usage:
deps.DepsholdsBuildStartListeners,BuildEndListeners, andOnChangeListeners, all typed*Listeners[T]. - Example:
deps/deps.go:350—type Listeners[T any]stores[]func(...T) bool.Notify()calls each listener; if the func returnsfalse, it is removed from the list (single-fire semantics).sync.Mutexprotects the slice. - Assessment: Clever self-pruning design. Generic parameterization (
Listeners[identity.Identity]vsListeners[any]) avoids casts. The bool return for auto-removal is an unusual but elegant idiom.
Categories assessment#
| Pattern | Present | Notes |
|---|---|---|
| Worker pools | Yes | para.Workers, rungroup.Group[T] |
| Fan-out/fan-in | Yes | rungroup: N workers drain shared channel |
| Pipeline processing | Yes | Build pipeline (process→assemble→render→post) is sequential, not channel-based |
| Context cancellation | Yes | 448 uses; first-class throughout resource fetch and server lifecycle |
| Graceful shutdown | Yes | signal.Notify(SIGINT, SIGTERM) in commandeer.go:421, server.go:942 |
| Rate limiting | Partial | semaphore.Weighted for rebuild mutual exclusion; time.Ticker for debounce |
Error handling#
- Style: Mixed — stdlib
fmt.Errorf %wwrapping (192 occurrences) plus a rich customherrorspackage for user-facing errors. Nopkg/errors. - Error types defined:
herrors.FileError(interface) — adds source position (file, line, column) and surrounding content context to any error. Implemented byfileError(private). MethodsUpdatePosition,UpdateContent,SetFilenameallow progressive enrichment as the error propagates up the call stack.herrors.TextSegmentError— pairs an error with the offending text segment (used in Markdown attribute parsing).herrors.TimeoutError— wraps a timeout with context.herrors.FeatureNotAvailableError— signals missing build-edition features (e.g., Sass without extended edition).common/herrors/errors.go:51— each is a concrete struct implementingerror.resources.HTTPError,config/security.AccessDeniedError,modules.goModuleError,hexec.NotFoundError.
- Wrapping approach:
fmt.Errorf("%w", err)exclusively (0 uses ofpkg/errors.Wrap). Custom errors implementUnwrap() errormanually where needed. - Enrichment pattern: Template and markup converters call
herrors.NewFileErrorFromPos(err, pos)to attach a source position. As the error propagates up, middleware layers callfe.UpdateContent(r, linematcher)to add surrounding line context for terminal display. - Examples:
markup/goldmark/blockquotes/blockquotes.go:121—herrors.NewFileErrorFromPos(err, bqctx.Position())markup/goldmark/codeblocks/render.go:101—&herrors.TextSegmentError{Err: err, Segment: attrStr}common/herrors/file_error.go:34—FileErrorinterface definition
Configuration pattern#
- Approach: Typed struct with
mapstructuredecoding. No functional options for the main config system; no Viper. - Structure:
config/allconfigdefines deeply nested Go structs (Config,RootConfig,Configs). Field tags drive mapstructure decoding from TOML/YAML maps. Sub-configs (markup, security, media) accessed viaGetConfigSection("markup"). - Consumers: All subsystems receive the
config.AllProviderinterface, not raw maps. This gives type-safe, IDE-navigable access without coupling to the concrete loader. - Functional options appear selectively (not for main config):
identity.ManagerOption/WithOnAddIdentity— for the dependency-tracking manager.resources/images/metadecoder options (WithFields,WithLatLongDisabled,WithDateDisabled,WithWarnLogger,WithSources).hugolib/filesystems.WithBaseFs.
- Example:
identity/identity.go:44—NewManager(opts ...ManagerOption)applies eachfunc(*identityManager)option before returning. Classic functional options pattern.
Dependency injection#
- Approach: Manual wiring via a central
Depsservice-locator struct. - Evidence:
deps/deps.go—Depsstruct holds references to every major subsystem:Fs,PathSpec,ContentSpec,TemplateStore,ResourceSpec,DynaCaches,FileCaches,warpc.Dispatchers.deps.DepsCfgis a configuration struct passed toNewDeps(cfg DepsCfg).deps.Deps.Clone()produces per-language copies sharing caches and template store.- No DI framework (no
wire,dig, orfx). All wiring is indeps/deps.go:Init()andhugolib.NewHugoSites().
- Assessment: The explicit manual pattern favors traceability and build-speed (no reflection at startup) at the cost of a god-object.
Depsis imported by essentially every package in the codebase. TheClone()approach elegantly handles the multi-language case without re-initialization overhead.
Other notable patterns#
Generics (Go 1.18+)#
Hugo adopted generics meaningfully, not just for toy wrappers. Key usages:
- Generic radix tree:
hugolib/doctree.Tree[T],SimpleTree[T],TreeThreadSafe[T]— the core page-tree data structure is fully generic, enabling type-safe storage ofpageState,pageMap, and other content types withoutanycasts. - Generic worker pool:
common/rungroup.Group[T]— typed channel-based worker pool; eliminates boxing. - Generic cache partitions:
cache/dynacache.Partition[K,V]— LRU cache partitions with typed keys and values. - Generic observer:
deps.Listeners[T]— typed listener lists. - Generic RPC messages:
internal/warpc.Message[T]— typed stdin/stdout message frames for WASM RPC. - Assessment: Hugo’s generics usage is architecturally motivated, not cosmetic. The doctree generic radix tree (
hugolib/doctree/simpletree.go:24-130) is the most impressive: it provides thread-safe and non-thread-safe variants behind a commonTree[T]interface, withiter.Seq2(Go 1.23 range-over-func) support.
Compile-Time Interface Satisfaction Checks#
- Usage:
var _ InterfaceName = (*ConcreteType)(nil)appears 20+ times inhugofs,markup,deps,internal/warpc. These cause a compile error if the concrete type drifts from its interface. - Example:
deps/deps.go:476—var _ identity.SignalRebuilder = (*BuildState)(nil);hugofs/rootmapping_fs.go:39—var _ ReverseLookupProvder = (*RootMappingFs)(nil). - Assessment: Standard Go idiom, consistently applied in the most interface-heavy packages. Particularly important in
hugofswhere afero wrapping creates many layered decorators.
Registry Pattern#
- Usage:
markup/markup.go:108—converterRegistrymaps MIME/format names toconverter.Providerinstances. Goldmark’s renderer uses a node-type-keyed registry for AST render hooks (reg.Register(ast.KindLink, func)). - Example:
markup/goldmark/render_hooks.go:143-146— fourreg.Register(ast.Kind*, r.render*)calls inRegisterFuncs. - Assessment: Clean registry-of-handlers pattern, not exposed externally (no runtime plugin registration). The render-hook registry enables goldmark’s extensibility within Hugo’s controlled environment.
Builder Pattern#
- Usage:
markup/tableofcontents.Builderaccumulates headings viaAddAt(h, row, level)then produces an immutable*FragmentsviaBuild(). Used during Markdown parsing where ToC structure is built incrementally. - Example:
markup/tableofcontents/tableofcontents.go:33-57 - Assessment: Correct use of builder to separate mutable construction from immutable result. The builder is not chained (no fluent interface) — methods return nothing. Straightforward.
Type Switches (155 occurrences)#
- Usage: Heavily used in template function dispatch, resource type assertions, config decoding, and filesystem unwrapping.
- Example:
hugofsfilesystem unwrapper:switch v := f.(type)to peel layers of afero decorator wrappers back to a base type. - Assessment: Necessary in a codebase with deep interface hierarchies and a flexible template language. Not a code smell given the architectural constraints; would be reduced if more interfaces used generics.
Codegen#
- Usage:
codegen/methods.gogenerates Go source for interface method lists (specifically for the enormousresources/page.Pageinterface). Run viago generateto keep generated shims in sync. - Assessment: A pragmatic escape valve from manual maintenance of a 100+ method interface. The alternative (reflection at runtime) would be slower and less type-safe.
Interface Embedding#
- Usage:
markup/converter/hooks/hooks.gocomposes context interfaces via embedding (BlockquoteContextembedsBaseContext;ImageLinkContextembedsLinkContext). TheTreeCommon[T]interface is embedded into bothTree[T]andTreeThreadSafe[T]to share method set. - Example:
markup/converter/hooks/hooks.go:87-96—BaseContextholdsPosition() text.Position; specialized contexts embed it. - Assessment: Follows interface segregation principle — narrow base interfaces composed into richer ones. Avoids duplicating method declarations across related interfaces.
Table-Driven Pattern (non-test)#
- Usage: Not prominently visible in non-test code. The build pipeline phases (process/assemble/render) are sequential function calls, not table-dispatched. Template namespaces are registered by iteration.
- Assessment: Table-driven style appears in tests; in production code, Hugo prefers explicit sequential calls or registry dispatch. No flag/dispatch tables found.