Gogs — Patterns#

Concurrency patterns#

Background worker loop (channel consumer)#

  • Usage: Three daemon goroutines started at boot consume from UniqueQueue channels
  • Example: internal/database/webhook.go:DeliverHooks() — ranges over HookQueue.Queue() channel; internal/database/mirror.go:SyncMirrors() — ranges over MirrorQueue.Queue()
  • Assessment: Clean and idiomatic. The worker runs a for repoID := range queue.Queue() loop, which blocks until a new item arrives and exits when the channel closes. Using a channel range as a blocking work queue is a solid Go pattern. However, there is no graceful shutdown: os.Signal handling is absent, and the goroutines run forever with no cancellation. The program exits abruptly on SIGTERM rather than draining in-flight work.

Timeout via channel and select#

  • Usage: The process package wraps exec.Cmd execution with a configurable deadline
  • Example: internal/process/manager.go:ExecDir() — spawns a goroutine that sends cmd.Wait() result to a done chan error, then selects on time.After(timeout) vs. <-done
  • Assessment: Idiomatic pre-context timeout pattern. The code predates wide adoption of context.WithTimeout for subprocess control. Modern Go would use exec.CommandContext, but this approach is correct for its era.

Email send confirmation channel#

  • Usage: internal/email/message.go attaches a confirmChan chan struct{} to each mail message for optional send acknowledgement
  • Example: internal/email/message.go:53confirmChan: make(chan struct{})
  • Assessment: Fine-grained but underused — it is wired into the struct but callers mostly fire-and-forget rather than waiting on the channel.

Goroutine launch (fire-and-forget)#

  • Usage: 6 naked go func launches total — very low for a project of this size
  • Example: InitDeliverHooks() and InitSyncMirrors() each consist of a single go statement wrapping a named function
  • Assessment: Sparse goroutine usage is deliberate: most concurrency is delegated to the background worker loops above. The low count reflects the monolithic, request-per-goroutine model of the Macaron HTTP stack.

Unique deduplication queue#

  • Usage: Custom UniqueQueue struct in internal/sync/unique_queue.go prevents the same repository ID from queuing multiple mirror syncs or webhook deliveries simultaneously
  • Example: internal/sync/unique_queue.go — wraps a chan string with a StatusTable (mutex-protected map[string]bool) to gate duplicate entries
  • Assessment: A thoughtful project-specific concurrency primitive. Rather than letting the same repo get redundant work requests queued up, AddFunc checks-and-sets atomically (under the StatusTable mutex) before pushing to the channel. The design is correct if slightly subtle.

No errgroup / fan-out / pipeline#

  • Usage: errgroup is absent; no fan-out/fan-in patterns; no pipeline processing
  • Assessment: Consistent with the project’s simplicity philosophy. Background work is serial within each worker loop.

Error handling#

  • Style: Mixed — dominant pattern is structured sentinel errors with errors.Is/errors.As; wrapping with github.com/cockroachdb/errors throughout
  • Error types defined:
    • ErrAccessTokenAlreadyExist, ErrLFSObjectNotExist, ErrLoginSourceNotExist, ErrTwoFactorNotFound — typed structs in internal/database implementing a NotFound() method
    • ErrOrgNotExist, ErrMissingIssueNumber — simple sentinel errors.New vars
    • internal/errx/errx.go defines a NotFound interface (NotFound() bool) and IsNotFound(err error) bool for behavioral error classification
    • internal/process/manager.go:ErrExecTimeout — package-level sentinel for subprocess timeout
  • Wrapping approach: github.com/cockroachdb/errors (errors.Wrap, errors.Wrapf, errors.Newf) is used consistently throughout cmd/ and most of internal/database. This is notable — it is neither the stdlib fmt.Errorf %w style nor the popular github.com/pkg/errors, but the CockroachDB fork which adds stack traces and richer context.
  • Examples:
    • cmd/gogs/backup.go:52errors.Wrap(err, "init configuration")
    • internal/database/lfs.go:49errors.As(err, &ErrLFSObjectNotExist{}) used to classify GORM ErrRecordNotFound into a domain error
    • internal/database/access_tokens.go:65errors.As(err, &ErrAccessTokenAlreadyExist{}) for uniqueness constraint classification
  • Pattern: The database layer converts low-level ORM errors (gorm.ErrRecordNotFound) into typed domain errors using errors.Is / errors.As, and callers use the errx.IsNotFound helper or errors.As to classify. This is clean and prevents ORM types from leaking to handlers.

Configuration pattern#

  • Approach: Package-level typed config vars (no functional options, no builder)
  • Example: internal/conf exports flat structs populated from INI: conf.Server.HTTPPort, conf.Auth.RequireSigninView, conf.SSH.StartBuiltinServer. These are package globals modified once at startup by conf.Init().
  • Assessment: This is a circa-2014 Go idiom that trades testability for simplicity. There is no way to inject a different config in tests — code must call conf.Init with a test INI file or work around it. No functional options anywhere in the codebase.

Dependency injection#

  • Approach: Manual wiring with global state; partial interface-based DI at the context middleware layer
  • Evidence:
    • Global: database.Handle (*DB) and database.x (xorm engine) are package-level vars set at startup and accessed directly by all handlers — ~80 handler files call database.Handle without injection.
    • Interface-based (partial): context.Store interface (internal/context/store.go) and context.AuthStore interface (internal/context/auth.go) are passed into context.Contexter(Store) at route registration time. Similarly, repo.Store interface (internal/route/repo/store.go) and lfs.Store interface (internal/route/lfs/store.go) allow the HTTP handler groups to receive injectable stores.
    • Handler struct (emerging): user.SettingsHandler uses a struct receiver with its store as a field — a newer pattern visible in a minority of handler files.
  • Assessment: The codebase is mid-evolution from global state to interface-based DI. Newer code (GORM stores, LFS, context middleware) uses interfaces injected at wiring time. Legacy code (xorm-based models, most route handlers) uses globals. The context.Store interface is the most complete example and is well-tested via mocks.

Other notable patterns#

sync.Once lazy initialization#

  • Prevalence: 10+ uses across internal/conf, internal/lazyregexp, internal/markup, internal/template, internal/email, internal/gitx
  • Style: A struct embeds sync.Once and exposes the computed value via a method that calls once.Do(build) on first access
  • Example: internal/lazyregexp/lazyre.go:20-25Regexp struct wraps a pattern string; Regexp() compiles it lazily on first call. Borrowed verbatim from the Go standard library with attribution.
  • Example 2: internal/conf/computed.goAppPath(), WorkDir(), CustomDir(), HomeDir() each use their own sync.Once to compute and cache path values
  • Assessment: Idiomatic and effective for expensive one-time initializations. The lazyregexp copy from Go stdlib is a good engineering choice — avoids init-time cost while keeping global regexp vars for readability.

StatusTable (mutex-protected boolean map)#

  • Package: internal/sync/status_table.go
  • Purpose: Tracks running/stopped state of named processes across goroutines using sync.RWMutex + map[string]bool
  • Assessment: Simple and correct. Used as the deduplication layer inside UniqueQueue. The read-mostly access pattern benefits from RWMutex (multiple concurrent IsRunning checks vs. serialized Start/Stop).

Middleware-as-closure (Macaron pattern)#

  • Prevalence: All middleware in internal/context/ returns macaron.Handler
  • Example: context.RepoAssignment(), context.Toggle(), context.OrgAssignment() — each is a factory function that captures configuration parameters in a closure and returns a func(*macaron.Context) or typed handler
  • Assessment: The Macaron reflection-based DI means handlers declare what they need as function arguments; the framework matches by type. The pattern is clean within Macaron’s model but non-portable: none of this middleware works with stdlib net/http or any other framework.

Type assertions / type switches#

  • Count: ~50 uses across the codebase
  • Example: internal/database/schemadoc/main.go:146 — asserting conn.Migrator().(interface{ ColumnTypes(...) }) to access extended interface methods; internal/database/webhook.gobean.(*HookTask) in xorm’s Iterate callback
  • Assessment: Most type assertions are xorm-driven (xorm Iterate passes interface{} beans). The custom assertion against an anonymous interface in schemadoc is a legitimate use of Go’s structural typing.

Build tags for optional features#

  • Usage: pam (Linux PAM auth) and cert (self-signed TLS generation) are compiled only with explicit build tags
  • Assessment: Appropriate use of build constraints for platform-specific and optional functionality. Keeps the default binary lean.

No generics, no functional options, no observer/event system#

  • Generics: Go version is 1.22 (go.mod); no generics used — consistent with a pre-1.18 codebase that has not been modernized
  • Functional options: Zero instances of func With... or type Option; all configuration via global vars or struct literal initialization
  • Observer/events: No pub-sub system; webhooks are the external event mechanism, delivered via the HookQueue worker loop

Table-driven tests#

  • Prevalence: 95 matches in test files — moderate use
  • Style: Named struct slices with t.Run(tc.name, ...) subtest execution
  • Assessment: The newer GORM-based store tests (internal/database/*_test.go) are well-structured with table-driven cases. The legacy xorm model tests are sparser.