Beego — Patterns#

Concurrency patterns#

Async Producer-Consumer Logging#

  • Usage: Core logging system (core/logs/log.go) uses a buffered channel to decouple log callers from actual I/O writers.
  • Example: core/logs/log.go:169bl.msgChan = make(chan *LogMsg, bl.msgChanLen). A dedicated goroutine consumes from msgChan in a select loop; callers write to the channel (with optional blocking on full). Flush and close signals use separate flushChan and closeChan channels.
  • Assessment: Idiomatic and effective. The separation of signal channels from the data channel is a clean pattern. Non-blocking send with fallback to blocking (select { case msgChan <- lm: default: msgChan <- lm }) is a reasonable approach for buffered async logging.

Goroutine-per-server Startup#

  • Usage: server/web/server.go launches the HTTP server (and optionally a separate admin server) each in its own goroutine; the main goroutine waits on a endRunning bool channel.
  • Example: server/web/server.go:167,210,240 — three goroutines: normal listen, TLS listen, and signal handler.
  • Assessment: Simple and straightforward; no worker pool needed here since there’s at most one server per goroutine.

Graceful Shutdown via Signals#

  • Usage: server/web/grace/ package implements process-level hot restart and graceful shutdown by trapping OS signals (SIGUSR1, SIGHUP, SIGTERM, etc.) and draining active connections before exit.
  • Example: server/web/grace/grace.go — a sigChan chan os.Signal receives OS signals; handlers registered per signal type trigger state transitions (StateShuttingDown → StateTerminate). A sync.WaitGroup tracks in-flight requests.
  • Assessment: Production-grade pattern. State machine approach (StateInit/Running/ShuttingDown/Terminate) is explicit and understandable.

sync.Pool for Request Context Reuse#

  • Usage: ControllerRegister (server/web/router.go:166) holds a sync.Pool to recycle per-request beecontext.Context objects.
  • Example: server/web/router.go:188pool: sync.Pool{New: func() interface{} { return beecontext.NewContext() }}. Pool is Get()’d in ServeHTTP and Put() in defer.
  • Assessment: Textbook pool usage for zero-allocation hot path. The pattern is correct but requires careful Reset() before returning to pool — the code does perform a reset on the context.

Compression Pool#

  • Usage: server/web/context/acceptencoder.go maintains two sync.Pool instances per compression type (custom level and best compression) for gzip/zlib writers.
  • Example: acceptencoder.go:118-130 — pools of gzip.Writer and zlib.Writer, reset via Reset(w) before use.
  • Assessment: Correct and efficient. Comments explicitly note “sync.Pool will not memory leak,” suggesting awareness of the pattern’s subtleties.

Singleflight Cache Decorator#

  • Usage: client/cache/singleflight.go wraps any Cache implementation with golang.org/x/sync/singleflight to collapse concurrent cache-miss loads.
  • Example: singleflight.go:50-58s.group.Do(key, ...) deduplicates concurrent loads for the same missing key; result is written back to cache before returning.
  • Assessment: Excellent use of the singleflight pattern. This prevents cache stampedes without the caller needing to know about it (pure decorator).

Task Scheduler Goroutines#

  • Usage: task/task.go runs a scheduler goroutine per Task that sleeps until the next trigger time, then fires the task in a new goroutine.
  • Example: task/task.go:596,604,624 — goroutines for start, stop, and changed-schedule signaling. A central scheduler goroutine (select { case <-ticker.C: ... }) dispatches task goroutines.
  • Assessment: Functional but lightweight — no goroutine pool, tasks spawn unbounded goroutines. Fine for low-frequency scheduled jobs; would need backpressure for high-frequency tasks.

Categories summary#

  • Worker pools: Not used — goroutines spawned directly for tasks and servers.
  • Fan-out/fan-in: Not found; each subsystem is independently concurrent.
  • Pipeline processing: Log pipeline (caller → channel → writer goroutine) is the clearest pipeline.
  • Context cancellation: 854 uses of context.Context — heavily used in ORM, httplib, cache. Standard Go idiom for cancellation propagation.
  • Graceful shutdown: Full implementation in server/web/grace/, using signal trapping and WaitGroup drain.
  • Rate limiting: Not built into the framework core; available as a user-registered filter.

Error handling#

  • Style: Mixed — custom error code system (berror) for framework errors, fmt.Errorf %w for wrapping, panic for programmer errors in ORM.
  • Error types defined:
    • berror.Code — numeric code type with a registry (core/berror/). Errors are formatted as "ERROR-{code}, {msg}" strings, making them parseable via berror.FromError().
    • core/validation.Error — field-level validation error with field name and message.
  • Wrapping approach:
    • Framework-internal errors use berror.Wrap(err, Code, msg) which delegates to fmt.Errorf("ERROR-%d, %s: %w", ...).
    • Ad-hoc errors in individual packages use plain fmt.Errorf("...: %w", err).
    • errors.Is / errors.As are used sparsely; berror.FromError parses the error string instead — a non-idiomatic choice that bypasses the Go error chain.
  • Panic usage: 123 panic() calls, concentrated in client/orm/. ORM uses panic for programmer errors (nil pointers, unsupported types, double-registration), consistent with the “don’t mask setup mistakes” school. Not used for runtime request errors.
  • Examples:
    • client/orm/orm_raw.go:300panic(errors.New("<RawSeter.QueryRow> All args must be use ptr")) for misuse.
    • client/orm/orm_raw.go:387return fmt.Errorf("Set raw error: %w", err) for runtime I/O errors.
    • core/berror/error.go:29berror.Error(c Code, msg string) wraps codes as formatted strings.

Configuration pattern#

  • Approach: Two distinct patterns coexist:
    1. Global config struct (BConfig *Config) — a large nested struct populated from conf/app.conf via reflection at startup. The canonical pattern for configuring the web server.
    2. Functional options — used in client/httplib for per-request and per-client configuration (ClientOption, BeegoHTTPRequestOption function types).
  • Example (functional options):
    // client/httplib/client_option.go:30
    func WithEnableCookie(enable bool) ClientOption {
        return func(client *Client) {
            client.Setting.EnableCookie = enable
        }
    }
    Options are applied via variadic ...ClientOption in constructors — idiomatic Go 1.13+ style.
  • Example (global struct): server/web/config.goBConfig is a public global; users set beego.BConfig.Listen.HTTPPort = 9090 directly. Simple but not safe for concurrent multi-server scenarios.

Dependency injection#

  • Approach: Two mechanisms, used in different layers:
    1. Global singletons — the web server layer uses package-level globals (BeeApp, BConfig, AppConfig). No DI framework; dependencies are found by package import.
    2. core/bean — an optional reflection-based IoC container with struct-tag injection (inject:"name"). Not used by the framework internals themselves; available for user applications.
  • Evidence:
    • server/web/server.go:init() creates BeeApp = NewHttpSever() — a global singleton wired at package load time.
    • core/bean/ provides RegisterBean, GetBean, and AutoWire (struct tag–based field injection via reflection).
  • Assessment: The dual approach reflects beego’s evolution. The singleton pattern is the heritage from v1; core/bean is a v2 addition for users who want explicit DI. The framework itself does not use bean internally — a slight inconsistency.

Other notable patterns#

init()-based Driver Registration (Side-Effect Imports)#

Every pluggable backend (cache drivers, config adapters, log adapters, session stores) registers itself via a package init() function:

// client/cache/redis/redis.go
func init() {
    cache.Register("redis", NewRedisCache)
}

Users activate a driver with a blank import: import _ "github.com/beego/beego/v2/client/cache/redis". This is idiomatic Go and keeps the core packages free of driver dependencies. The same pattern appears in core/config, core/logs, server/web/session, and client/orm.

Registry Pattern#

Used universally for extensible subsystems. A Register(name string, factory Func) function maps names to factory functions at init() time. At use time, a NewXxx(adapter string, config string) function looks up the factory by name and calls it. The ORM model registry is a variant: orm.RegisterModel(models...) stores reflection metadata for struct → table mapping.

Generics for Handler Wrappers (Go 1.18+)#

server/web/generic_wrapper.go uses type parameters to provide type-safe HTTP handler adapters:

func WrapperFromJson[T any](biz bizFunc[T]) func(ctx *context.Context) {
    return internalWrapper(biz, func(ctx *context.Context) (params T, err error) {
        err = ctx.BindJSON(&params)
        return
    })
}

This allows writing handlers as func(ctx, MyRequest) (any, error) without type assertions — a clean addition that avoids reflection at the handler call site.

Decorator Pattern (Cache Layer)#

client/cache/ ships several decorators that wrap Cache to add behavior without modifying the base:

  • SingleflightCache — deduplicates concurrent misses.
  • ReadThroughCache — loads on miss and writes back.
  • WriteThroughCache — writes to origin and cache atomically.
  • WriteDeleteCache — deletes cache on write.
  • BloomFilterCache — pre-filters misses with a bloom filter. All follow the same pattern: embed Cache, override specific methods, delegate the rest. This is a textbook decorator stack and is the richest use of composition in the codebase.

Hook / Observer Pattern (Startup Hooks)#

server/web/beego.go exposes AddAPPStartHook(fns ...hookfunc) which appends to a slice of func() error callbacks. These run once inside initBeforeHTTPRun() (guarded by sync.Once). The framework’s own subsystems (session, template, gzip, admin) register via this same mechanism — uniform treatment of internal and user-registered hooks.

Onion-style Middleware (FilterChain)#

Beyond the five-point filter pipeline, FilterChain is a func(next FilterFunc) FilterFunc type — middleware that wraps the next handler. Chains are composed via:

// server/web/router.go
for i := len(filterChains) - 1; i >= 0; i-- {
    root = filterChains[i](root)
}

This is the standard onion/middleware composition idiom, functionally equivalent to net/http middleware chains.

Table-driven Tests#

69 instances of testCases/tt.Run patterns across test files. Moderate adoption — used in routing, ORM, config, and cache packages but not universally. No single framework enforced (stdlib testing used throughout).

Interface Embedding (ORM types)#

client/orm/types.go shows fine-grained interface decomposition: TxBeginner, TxCommitter, txer, txEnder are small single-purpose interfaces that compose into the full TxOrmer. This follows the Interface Segregation Principle well.

Reflection-heavy ORM Bootstrap#

client/orm uses reflect extensively at model registration time to introspect struct fields, tags, and relationships. This is front-loaded: all reflection happens at startup via RegisterModel() and orm.RunSyncdb(), not per-query. The hot path uses pre-built metadata.