Gitea — Patterns#

Concurrency patterns#

Worker pool via WorkerPoolQueue[T any]#

  • Usage: Central mechanism for all async operations. Every background task (webhook delivery, search indexing, email sending, automerge scheduling, Actions dispatch) goes through a WorkerPoolQueue[T any] instance. At least 4 queue instances observed: statsQueue, issueIndexerQueue, indexerQueue, plus webhook and task queues.
  • Example: modules/queue/workerqueue.goWorkerPoolQueue[T] holds batchChan chan []T, workerNum/workerMaxNum counts, and a workerNumMu sync.Mutex. Workers pop batches from batchChan and call safeHandler(batch).
  • Assessment: Idiomatic and well-designed. Generics (Go 1.21) make the type safe without reflection, and the pluggable backend (channel / LevelDB / Redis) is cleanly hidden behind a baseQueue interface. One concern: workerNumMu is a plain mutex rather than sync/atomic, which may create contention under load. Overall, this is the most architecturally significant pattern in the codebase.

Graceful shutdown coordination#

  • Usage: modules/graceful.Manager is the single shutdown authority. All long-running components call graceful.GetManager().ShutdownContext() to obtain a cancellable context. Hard shutdown uses HammerContext(). Components register cleanup via RunAtShutdown() / RunAtTerminate().
  • Example: routers/web/events/events.go:43 — SSE event loop holds shutdownCtx := graceful.GetManager().ShutdownContext() and exits when it’s cancelled. modules/graceful handles SIGTERM and SIGUSR1 (hot-reload via socket inheritance).
  • Assessment: Clean pattern. Having a single manager rather than per-component done channels prevents missed shutdowns. The numberOfServersToCreate = 4 constant is the only wart — a magic number that must be incremented manually when adding server components.

errgroup for parallel sub-tasks#

  • Usage: Targeted use in two places: modules/markup/render.go (parallel rendering of multiple markup blocks) and modules/git/pipeline/namerev.go (parallel git name-rev operations).
  • Example: modules/markup/render.go:260eg, _ := errgroup.WithContext(ctx) fans out render work then collects errors.
  • Assessment: Appropriate scope — used only where bounded parallelism with error propagation is needed, not sprinkled everywhere.

Channel-based cancellation and signaling#

  • Usage: select appears 137 times; make(chan in ~10 production locations. Channels are used for done signals (done := make(chan struct{})) and for pipelining (pointerChan := make(chan lfs.PointerBlob)), not as a general-purpose concurrency primitive.
  • Example: routers/web/repo/view.go:158 — a done channel signals when an async blame computation has finished so the handler can proceed.
  • Assessment: Restrained use of channels is appropriate for a codebase this size. Over-reliance on channels instead of mutexes would make the code harder to follow.

sync.Once / sync.OnceValue for lazy initialization#

  • Usage: ~10 occurrences. sync.OnceValue (Go 1.21) is used in the request context (routers/common/pagetmpl.go:80-81) to lazily compute per-request data (notification count, active stopwatch) only if the template actually accesses it.
  • Example: routers/common/pagetmpl.go:80data.GetNotificationUnreadCount = sync.OnceValue(func() int64 { return notificationUnreadCount(ctx) }). The template calls a function; the function is idempotent.
  • Assessment: Clever use of sync.OnceValue for request-scoped lazy evaluation. Avoids computing expensive per-request data that may not be needed for every page.

sync primitives (mutex, atomic)#

  • Usage: 187 total occurrences of sync.Mutex, sync.RWMutex, sync.Once, sync.WaitGroup, sync.Map, atomic.*. Heavy use.
  • Assessment: Mutex usage is concentrated in worker infrastructure (WorkerPoolQueue.workerNumMu) and module-level singletons. sync/atomic.Int64 is used in WorkerPoolQueue for the shutdown timeout. Generally appropriate.

Error handling#

  • Style: Mixed but principled. Wrapping with fmt.Errorf %w is the dominant approach (1326 occurrences). Sentinel-style custom error types exist for specific subsystems. errors.Is / errors.As are used extensively for inspection (451 occurrences combined).

  • Error types defined: 15+ custom types including:

    • modules/process.Error — process manager errors
    • modules/git/gitcmd.runStdError / pipelineError — git subprocess errors with stderr captured
    • modules/structs.APIError / SearchError / LFSLockError — API response error shapes
    • modules/lfs.ObjectError — LFS object errors
    • routers/api/packages/container.ContainerError — OCI registry error format
    • modules/templates/eval.ExprError — template expression evaluation errors
  • Wrapping approach: fmt.Errorf("...: %w", err) is standard throughout. pkg/errors is not used (only stdlib). Error context strings consistently include the operation and parameters.

  • Examples:

    • contrib/backport/backport.go:229fmt.Errorf("unable to xdg-open to %s: %w", url, err) — wraps with context
    • modules/git/gitcmd/error.gorunStdError preserves stderr output from git subprocess, implements RunStdError interface so callers can extract raw git error messages via errors.As
    • services/migrations/error.go:17IsRateLimitError uses a type assertion helper rather than errors.As (older pattern, pre-errors.Is/As era)

Configuration pattern#

  • Approach: Global config struct / package-level variables via modules/setting. The INI file (app.ini) is parsed once at startup into typed package-level variables (setting.Database, setting.SSH, setting.AppURL, etc.). There is no functional options pattern at the application-configuration level.

  • Functional options: Used selectively for client configuration in self-contained modules:

    • modules/hcaptchaClientOption func(*Client) with WithHTTP(...), WithContext(...)
    • modules/auth/password/pwn — same pattern
    • modules/gtprof, modules/actions/jobparserWithAttributes(...), WithJobResults(...)
  • Example: A component reading configuration uses package globals: if setting.SSH.Enabled { ... }. External HTTP clients use functional options for test-injected transports: NewClient(WithHTTP(mockTransport)).

  • Assessment: The global-var config model works for a monolith but creates invisible coupling and makes unit-testing configuration-sensitive code harder. The selective use of functional options for external clients is good practice.


Dependency injection#

  • Approach: Manual wiring. No framework (wire, dig, fx).

  • Evidence:

    • 196 func init() package-level initializers. Modules self-register by importing packages as side effects.
    • routers.InitWebInstalled() is a 24-step explicit initialization sequence (see architecture doc). Each step calls a named init-style function.
    • Singletons exposed via GetManager() pattern: graceful.GetManager(), process.GetManager(), queue.GetManager().
    • services/notify.RegisterNotifier(notifier) — notifiers self-register during routers.InitWebInstalled().
  • Assessment: The explicit init sequence is actually readable and debuggable — you can trace exactly what order things start in. The downside is 196 init() functions scattered across packages; execution order depends on import graph, which is not always obvious. The singleton GetManager() pattern is a testability concern.


Other notable patterns#

Observer / event system (services/notify)#

services/notify implements a classic observer via a package-level []Notifier slice. RegisterNotifier(n Notifier) appends the notifier and calls go n.Run(). Every domain event (issue created, wiki edited, package deleted, job status updated) calls the corresponding notify.XXX(ctx, ...) function, which fans out to all registered notifiers synchronously. Implementations include the mailer, webhook dispatcher, and Gitea Actions bridge.

  • Example: services/notify/notify.go:22-24RegisterNotifier starts the notifier’s background goroutine, then notify.NewWikiPage(...) iterates the slice.
  • Assessment: Simple and effective for a monolith. The synchronous fan-out means a slow notifier blocks the calling goroutine; the actual work is async because each notifier has its own goroutine processing a queue internally.

Generics (WorkerPoolQueue[T], bind[T], errorAs[T])#

Targeted, practical use of generics introduced in Go 1.18/1.21:

  • modules/queue.WorkerPoolQueue[T any] — eliminates the interface{} + reflection pattern from earlier Gitea queue code.
  • routers/api/v1/api.go:737func bind[T any](_ T) any returns an empty instance for chi’s type-safe request binding.
  • routers/web/repo/editor_error.go:19func errorAs[T error](v error) (e T, ok bool) is a thin typed wrapper over errors.As.
  • routers/api/packages/nuget.TypedValue[T any] — typed OData value container.
  • Assessment: Generics use is disciplined and adds genuine type safety without over-engineering. Not yet pervasive, which is appropriate for a codebase that predates Go 1.18 and has incremental adoption.

Table-driven tests#

Heavy use: 242 occurrences of testCases, tt.Run, tc.name patterns in *_test.go files. Standard Go idiom; no surprises.

init() function proliferation#

196 func init() calls is on the high side for a Go project. They are used both for legitimate package initialization (registering renderers, auth providers, template functions) and as a substitute for explicit wiring. The side-effect-import pattern (import _ "code.gitea.io/gitea/routers/common") makes boot-time dependencies implicit.

Interface embedding / composition#

Multiple interfaces in modules/markup/renderer.go compose via embedding:

type PostProcessRenderer interface { Renderer; ... }
type ExternalRenderer   interface { Renderer; ... }

modules/git/catfile_batch.go uses a similar pattern for CatFileBatchCloser. This is idiomatic Go interface segregation.

Type switches#

54 type switch (switch x.(type)) usages — moderate. Used primarily in the git subsystem and markup renderers to dispatch on concrete types without reflecting.

sync.OnceValue for request-scoped lazy computation#

Notable Go 1.21 feature adoption in routers/common/pagetmpl.go — instead of computing per-page metadata eagerly, functions wrapped with sync.OnceValue are stored in the template data struct and called only if the template references them. Clean, zero-overhead pattern.