restic — Patterns#
Concurrency patterns#
Worker Pool via errgroup + Channel#
- Usage: The primary parallelism pattern throughout the codebase; used in
archiver,restorer,data, and the domainresticpackage. - Example:
internal/archiver/file_saver.go:34-55—newFileSavercreates a buffered channelch, then launchesfileWorkersgoroutines viaerrgroup.Group.Go. Workers range over the channel until it is closed. - Assessment: Idiomatic and effective. The errgroup ensures all workers are joined and the first error propagates context cancellation to the rest. Shutdown is triggered by closing the input channel (
TriggerShutdown), which terminates the range loop.
Fan-out/Fan-in via errgroup.WithContext + Channel#
- Usage:
internal/restic/parallel.go—ParallelListimplements a generic fan-out: one producer goroutine lists file IDs and sends them to a channel; N worker goroutines consume and process them. - Example:
internal/restic/parallel.go:11-50—wg, ctx := errgroup.WithContext(ctx)creates a linked context; the producer closes the channel after listing; workers return errors that cancel the shared context. - Assessment: Clean textbook fan-out. The use of
select { case <-ctx.Done(): ... case ch <- ...: }in the producer avoids a deadlock if a worker returns early. Used as a utility function across the codebase.
Semaphore via Buffered Channel#
- Usage:
internal/backend/sema/semaphore.go— a struct wrappingchan struct{}of capacity N. Used bysema.Backendto limit concurrent backend operations. - Example:
internal/backend/sema/semaphore.go:12-31—GetToken()sends to the channel;ReleaseToken()receives from it. Capacity equals max connections (default 5). - Assessment: The canonical Go semaphore idiom. Exposed as a decorator (
sema.Backend) so all storage drivers get connection limiting without any driver code change. Also used in SFTP startup (sftp.go:175).
errgroup as the Standard Async Coordination Primitive#
- Usage: 18+ call sites across non-test code;
golang.org/x/sync/errgroupis the project’s exclusive higher-level concurrency abstraction. - Assessment: restic deliberately avoids raw
sync.WaitGroupfor error-bearing goroutines.errgroup.WithContextpairs cancellation with error collection cleanly. Thewg.SetLimit()(used inParallelRemove) is also exploited, reducing boilerplate for bounded concurrency.
Context Cancellation as the Primary Stop Signal#
- Usage: 769 occurrences of
context.Contextin the codebase. Every I/O-bound function in the backend, repository, archiver, and restorer layers accepts a context. - Example:
internal/walker/walker.go:59-61— after each tree node:if ctx.Err() != nil { return ctx.Err() }. This is a consistent pattern: check context at the start of every iteration step. - Assessment: Thorough and idiomatic. Context cancellation is the uniform way to communicate shutdown/timeout to all layers; there is no ad-hoc “stop” flag.
Graceful Shutdown via Signal → Context Cancel#
- Usage:
cmd/restic/cleanup.go—createGlobalContextwiresSIGINT/SIGTERMto acontext.CancelFunc. - Example:
cleanup.go:14-38— a dedicated goroutine blocks on a signal channel; on receipt it callscancel()and logs the signal. The root cobra context propagates this cancellation to all subcommands. - Assessment: Single, clean shutdown path. Restic also supports
RESTIC_DEBUG_STACKTRACE_SIGINTto dump a full goroutine stack on SIGINT — a production debugging affordance.
Rate Limiting via io.Reader/io.Writer Decoration#
- Usage:
internal/backend/limiter— aLimiterinterface wrappingio.Reader/io.Writerfor upload/download throttling; injected into the transport layer. - Assessment: Rather than a token-bucket goroutine, the rate limiter wraps I/O streams, keeping it transparent to backend logic.
Error handling#
- Style: Mixed —
github.com/pkg/errors(stack-trace wrapping) via theinternal/errorsfacade, plus stdliberrors.Is/errors.Asfor inspection. - Error types defined:
fatalError(internal/errors/fatal.go) — wraps a message to signal CLI termination; detected viaerrors.IsFatal()inmain(). Errors returned by command handlers are inspected here;fatalErrortriggers a non-zero exit without printing the error chain.MultipleIDMatchesError,NoIDByPrefixError(internal/restic/backend_find.go) — typed errors for snapshot ID prefix resolution.alreadyLockedError,invalidLockError(internal/restic/lock.go) — sentinel types for lock management.checker.Error,checker.TreeError— structured errors from repository integrity checks.repository/pack.InvalidFileError,repository/checker.PackError— typed storage errors.
- Wrapping approach:
errors.Wrap/errors.Wrapffromgithub.com/pkg/errors(121 call sites in non-test code) — used for errors from external calls where a stack trace is valuable.fmt.Errorfwith%w(77 call sites) — used for simple contextual additions, usually in thecmd/layer.errors.Is/errors.As(136 call sites) — used for all error type inspection.
- The
internal/errorsfacade: restic does not importgithub.com/pkg/errorsdirectly in most packages. Instead it importsgithub.com/restic/restic/internal/errorswhich re-exportsWrap,Wrapf,New,Errorffrompkg/errorsandAs,Is,Join,Unwrapfrom stdlib. This allows switching the stack-trace implementation without touching call sites. - Examples:
cmd/restic/cmd_dump.go:186—fmt.Errorf("cannot dump to file: %w", err)— simple context addition.cmd/restic/cmd_self_update.go:68—errors.Wrap(err, "unable to find executable")— external error with stack trace.cmd/restic/cmd_mount.go:138—errors.Is(err, os.ErrNotExist)— sentinel error check.
Configuration pattern#
- Approach: Per-command
Optionsstructs bound to pflag flags via cobra. Every command defines its ownXxxOptionsstruct (e.g.BackupOptions,RestoreOptions,CheckOptions) and registers flags on the cobra command’sFlagSet. Global configuration lives inglobal.Optionsand is passed as a value everywhere. - Example:
internal/archiver/archiver.go:141-150—Optionsstruct with fieldsReadConcurrency,MaxTreePackSize,NoExtraVerify,PackSize. Passed as a value argument toarchiver.New(repo, fs, opts). - No functional options pattern: restic uses plain constructor functions
New(...)with anOptionsvalue arg rather thanWith*variadic options. This is consistent acrossarchiver,repository,restorer. - Feature flags:
internal/featureimplements anAlpha/Beta/Stable/Deprecatedflag lifecycle.RESTIC_FEATURESenv var can enable/disable named flags at startup. Example:feature.Flag.Enabled(feature.BackendErrorRedesign).
Dependency injection#
- Approach: Manual constructor-based wiring. No DI framework (
wire,dig,fx). - Evidence:
internal/global/global.gois the composition root.OpenRepositoryexplicitly constructs the decorator stack:
Returns adriver → sema.NewBackend → logger.New → retry.New → optional cache*repository.Repositoryvalue. Commands receive this fully wired value. No service locator, no ambient global state beyondglobal.Options. - Backend registry: The
location.Registry(populated viainit()side effects frombackend/all) acts as a factory; URL-scheme dispatch replaces DI for backend selection at runtime.
Other notable patterns#
Decorator / Middleware Pattern on an Interface (Backend Stack)#
The most architecturally distinctive pattern in restic. backend.Backend is a 12-method interface. Decorators (sema, logger, retry, cache, dryrun, limiter) each wrap a Backend and implement the same interface. global.wrapBackend composes them explicitly. Each decorator also implements backend.Unwrapper (single Unwrap() Backend method), enabling the generic AsBackend[T] function to walk the chain and extract any layer by type.
// internal/backend/backend.go:109
func AsBackend[B Backend](b Backend) B {
for b != nil {
if be, ok := b.(B); ok {
return be
}
if be, ok := b.(Unwrapper); ok {
b = be.Unwrap()
} else {
break
}
}
var be B
return be
}This is an HTTP middleware stack applied to a storage interface, with generic type-safe introspection.
Generics for Type-Safe File Type Constraints#
internal/restic/repository.go:99-154—FileTypesis a union constraintFileType | WriteableFileType. Generic interfacesSaverUnpacked[FT FileTypes],RemoverUnpacked[FT FileTypes],Unpacked[FT FileTypes]enforce at compile time which parts of the repository allow writes.ParallelRemove[FT FileTypes]is a generic function parameterized on this constraint.internal/backend/location/registry.go:40—genericBackendFactory[C any, T backend.Backend]captures config type and backend type generically.internal/restorer/hardlinks_index.go—HardlinkIndex[T any]is a generic map keyed by inode/device.- Assessment: Targeted, purposeful generics use — not generics for abstraction’s sake. The
FileTypesconstraint replaces what would have been a runtime check or aWriteableFileTypebool field.
Go 1.23 Range-over-Function Iterators (iter.Seq)#
internal/data/tree.go:36—type TreeNodeIterator = iter.Seq[NodeOrError]— tree node iteration via a push-style iterator.internal/data/tree.go:368—DualTreeIteratortakes twoTreeNodeIterators and zips them.internal/repository/index/associated_data.go:153—All()andKeys()onAssociatedSet[T]returniter.Seq2/iter.Seq.cmd/restic/cmd_copy.go:79-80— a snapshot filter returnsiter.Seq[*data.Snapshot].- Assessment: Adoption of Go 1.23 iterator protocol is incremental but deliberate, replacing channel-based iteration in tree traversal to eliminate goroutine overhead for sequential consumers.
Visitor with Optional Callbacks (Struct-based Visitor)#
internal/walker/walker.go:26-32—WalkVisitoris a struct with callback fields:Callers fill only the callbacks they need. This avoids both the interface-implement-all-methods burden and the function explosion of separatetype WalkVisitor struct { ProcessNode WalkFunc // mandatory LeaveDir func(path string) error // optional }Walkvariants.- Assessment: Practical middle ground between the interface visitor pattern and bare function callbacks. Widely used across
cmd_find,cmd_ls,cmd_stats,dump.
Sentinel Error for Control Flow in Recursive Traversal#
internal/walker/walker.go:13—var ErrSkipNode = errors.New("skip this node")— ifProcessNodereturnsErrSkipNodeon a directory, it is not descended into.- Assessment: Standard Go pattern (cf.
filepath.SkipDir). The named sentinel avoids a separate bool return and keeps the error path singular.
Build-Tag Dual-File Feature Gating#
- Used for FUSE mount, debug commands, and self-update: every optional command has two files:
cmd_mount.go(real implementation,//go:build !nofuse)cmd_mount_disabled.go(stub that registers the command but returns a helpful error,//go:build nofuse)
- Assessment: Avoids
#ifdef-style conditional code inside files. Makes binary footprint controllable at build time without dead code.
sync.Once for Lazy Initialization#
internal/repository/repository.go:52-53—allocEnc,allocDec sync.Onceinitialize zstd compressor/decompressor pools on first use.internal/restic/config.go:55—checkPolynomialOnceverifies the CDC polynomial once.internal/ui/termstatus/status.go:35—outputWriterOnceinitializes the terminal writer.- Assessment: Standard usage. Avoids init-time side effects for expensive allocations.
Atomic Types for Concurrent Progress Counters#
internal/ui/progress/counter.go:20—value, max atomic.Uint64— used by progress bars updated from multiple goroutines without a mutex.internal/restorer/restorer.go:675—atomic.AddUint64(&nchecked, 1)— inline lock-free counter increment.- Assessment: Appropriate use for hot path counters. No overuse; mutexes are used where the critical section spans more than a single integer update.
interface{} → any Migration in Progress#
interface{}appears 145 times,any116 times — the codebase is mid-migration to the Go 1.18anyalias. No mechanical consistency yet, but newer code usesany.