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.goWorkers.sem is a buffered channel acting as a semaphore. Run(fn) blocks until a slot is free, then launches the task in a goroutine. errgroup.Group collects errors.
  • Assessment: Clean, idiomatic. The semaphore-over-channel approach is the standard Go idiom for bounded goroutine pools. The interface Runner (with Run and Wait) 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 channel chan T. The caller enqueues work items; workers call a user-supplied Handle(ctx, T) error func.
  • Example: common/rungroup/rungroup.goRun[T](ctx, Config[T]) Group[T]. Uses Go generics for full type safety; no interface{} 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 over para for 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:946wg1, ctx := errgroup.WithContext(context.Background()) launches HTTP listener and WebSocket server as goroutines; context cancel propagates shutdown.
  • Assessment: Standard and correct usage. Hugo uses errgroup exclusively for structured concurrency (no raw go func for error-collecting work).

Event Debouncing via Batcher#

  • Usage: watcher/batcher.go accumulates fsnotify.Event items until a ticker fires, then emits the full batch. Prevents per-file rebuild thrashing during rapid saves.
  • Example: watcher/batcher.go:52-66run() goroutine selects on FileWatcher.Events(), ticker.C, and done. On tick: if events accumulated, send batch; reset slice.
  • Assessment: Idiomatic ticker+select debounce. The buffered Events chan []fsnotify.Event prevents back-pressure from the consumer. A well-worn pattern, cleanly packaged.

Semaphore via semaphore.Weighted#

  • Usage: commands/hugobuilder.go:70fullRebuildSem *semaphore.Weighted (weight=1) ensures at most one full rebuild runs concurrently, even if multiple file-change events arrive simultaneously.
  • Example: commands/server.go:103fullRebuildSem: semaphore.NewWeighted(1).
  • Assessment: Correct use of golang.org/x/sync/semaphore for mutual exclusion between long-running operations. Preferable to a sync.Mutex here because TryAcquire allows skipping queued rebuilds.

Observer/Listener Pattern with Self-Removing Generics#

  • Usage: deps.Deps holds BuildStartListeners, BuildEndListeners, and OnChangeListeners, all typed *Listeners[T].
  • Example: deps/deps.go:350type Listeners[T any] stores []func(...T) bool. Notify() calls each listener; if the func returns false, it is removed from the list (single-fire semantics). sync.Mutex protects the slice.
  • Assessment: Clever self-pruning design. Generic parameterization (Listeners[identity.Identity] vs Listeners[any]) avoids casts. The bool return for auto-removal is an unusual but elegant idiom.

Categories assessment#

PatternPresentNotes
Worker poolsYespara.Workers, rungroup.Group[T]
Fan-out/fan-inYesrungroup: N workers drain shared channel
Pipeline processingYesBuild pipeline (process→assemble→render→post) is sequential, not channel-based
Context cancellationYes448 uses; first-class throughout resource fetch and server lifecycle
Graceful shutdownYessignal.Notify(SIGINT, SIGTERM) in commandeer.go:421, server.go:942
Rate limitingPartialsemaphore.Weighted for rebuild mutual exclusion; time.Ticker for debounce

Error handling#

  • Style: Mixed — stdlib fmt.Errorf %w wrapping (192 occurrences) plus a rich custom herrors package for user-facing errors. No pkg/errors.
  • Error types defined:
    • herrors.FileError (interface) — adds source position (file, line, column) and surrounding content context to any error. Implemented by fileError (private). Methods UpdatePosition, UpdateContent, SetFilename allow 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 implementing error.
    • resources.HTTPError, config/security.AccessDeniedError, modules.goModuleError, hexec.NotFoundError.
  • Wrapping approach: fmt.Errorf("%w", err) exclusively (0 uses of pkg/errors.Wrap). Custom errors implement Unwrap() error manually 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 call fe.UpdateContent(r, linematcher) to add surrounding line context for terminal display.
  • Examples:
    • markup/goldmark/blockquotes/blockquotes.go:121herrors.NewFileErrorFromPos(err, bqctx.Position())
    • markup/goldmark/codeblocks/render.go:101&herrors.TextSegmentError{Err: err, Segment: attrStr}
    • common/herrors/file_error.go:34FileError interface definition

Configuration pattern#

  • Approach: Typed struct with mapstructure decoding. No functional options for the main config system; no Viper.
  • Structure: config/allconfig defines deeply nested Go structs (Config, RootConfig, Configs). Field tags drive mapstructure decoding from TOML/YAML maps. Sub-configs (markup, security, media) accessed via GetConfigSection("markup").
  • Consumers: All subsystems receive the config.AllProvider interface, 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/meta decoder options (WithFields, WithLatLongDisabled, WithDateDisabled, WithWarnLogger, WithSources).
    • hugolib/filesystems.WithBaseFs.
  • Example: identity/identity.go:44NewManager(opts ...ManagerOption) applies each func(*identityManager) option before returning. Classic functional options pattern.

Dependency injection#

  • Approach: Manual wiring via a central Deps service-locator struct.
  • Evidence:
    • deps/deps.goDeps struct holds references to every major subsystem: Fs, PathSpec, ContentSpec, TemplateStore, ResourceSpec, DynaCaches, FileCaches, warpc.Dispatchers.
    • deps.DepsCfg is a configuration struct passed to NewDeps(cfg DepsCfg).
    • deps.Deps.Clone() produces per-language copies sharing caches and template store.
    • No DI framework (no wire, dig, or fx). All wiring is in deps/deps.go:Init() and hugolib.NewHugoSites().
  • Assessment: The explicit manual pattern favors traceability and build-speed (no reflection at startup) at the cost of a god-object. Deps is imported by essentially every package in the codebase. The Clone() 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 of pageState, pageMap, and other content types without any casts.
  • 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 common Tree[T] interface, with iter.Seq2 (Go 1.23 range-over-func) support.

Compile-Time Interface Satisfaction Checks#

  • Usage: var _ InterfaceName = (*ConcreteType)(nil) appears 20+ times in hugofs, markup, deps, internal/warpc. These cause a compile error if the concrete type drifts from its interface.
  • Example: deps/deps.go:476var _ identity.SignalRebuilder = (*BuildState)(nil); hugofs/rootmapping_fs.go:39var _ ReverseLookupProvder = (*RootMappingFs)(nil).
  • Assessment: Standard Go idiom, consistently applied in the most interface-heavy packages. Particularly important in hugofs where afero wrapping creates many layered decorators.

Registry Pattern#

  • Usage: markup/markup.go:108converterRegistry maps MIME/format names to converter.Provider instances. 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 — four reg.Register(ast.Kind*, r.render*) calls in RegisterFuncs.
  • 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.Builder accumulates headings via AddAt(h, row, level) then produces an immutable *Fragments via Build(). 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: hugofs filesystem 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.go generates Go source for interface method lists (specifically for the enormous resources/page.Page interface). Run via go generate to 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.go composes context interfaces via embedding (BlockquoteContext embeds BaseContext; ImageLinkContext embeds LinkContext). The TreeCommon[T] interface is embedded into both Tree[T] and TreeThreadSafe[T] to share method set.
  • Example: markup/converter/hooks/hooks.go:87-96BaseContext holds Position() 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.