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:169—bl.msgChan = make(chan *LogMsg, bl.msgChanLen). A dedicated goroutine consumes frommsgChanin aselectloop; callers write to the channel (with optional blocking on full). Flush and close signals use separateflushChanandcloseChanchannels. - 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.golaunches the HTTP server (and optionally a separate admin server) each in its own goroutine; the main goroutine waits on aendRunningbool 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— asigChan chan os.Signalreceives OS signals; handlers registered per signal type trigger state transitions (StateShuttingDown → StateTerminate). Async.WaitGrouptracks 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 async.Poolto recycle per-requestbeecontext.Contextobjects. - Example:
server/web/router.go:188—pool: sync.Pool{New: func() interface{} { return beecontext.NewContext() }}. Pool isGet()’d inServeHTTPandPut()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.gomaintains twosync.Poolinstances per compression type (custom level and best compression) for gzip/zlib writers. - Example:
acceptencoder.go:118-130— pools ofgzip.Writerandzlib.Writer, reset viaReset(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.gowraps anyCacheimplementation withgolang.org/x/sync/singleflightto collapse concurrent cache-miss loads. - Example:
singleflight.go:50-58—s.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.goruns a scheduler goroutine perTaskthat 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 %wfor wrapping,panicfor 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 viaberror.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 tofmt.Errorf("ERROR-%d, %s: %w", ...). - Ad-hoc errors in individual packages use plain
fmt.Errorf("...: %w", err). errors.Is/errors.Asare used sparsely;berror.FromErrorparses the error string instead — a non-idiomatic choice that bypasses the Go error chain.
- Framework-internal errors use
- Panic usage: 123
panic()calls, concentrated inclient/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:300—panic(errors.New("<RawSeter.QueryRow> All args must be use ptr"))for misuse.client/orm/orm_raw.go:387—return fmt.Errorf("Set raw error: %w", err)for runtime I/O errors.core/berror/error.go:29—berror.Error(c Code, msg string)wraps codes as formatted strings.
Configuration pattern#
- Approach: Two distinct patterns coexist:
- Global config struct (
BConfig *Config) — a large nested struct populated fromconf/app.confvia reflection at startup. The canonical pattern for configuring the web server. - Functional options — used in
client/httplibfor per-request and per-client configuration (ClientOption,BeegoHTTPRequestOptionfunction types).
- Global config struct (
- Example (functional options):Options are applied via variadic
// client/httplib/client_option.go:30 func WithEnableCookie(enable bool) ClientOption { return func(client *Client) { client.Setting.EnableCookie = enable } }...ClientOptionin constructors — idiomatic Go 1.13+ style. - Example (global struct):
server/web/config.go—BConfigis a public global; users setbeego.BConfig.Listen.HTTPPort = 9090directly. Simple but not safe for concurrent multi-server scenarios.
Dependency injection#
- Approach: Two mechanisms, used in different layers:
- Global singletons — the web server layer uses package-level globals (
BeeApp,BConfig,AppConfig). No DI framework; dependencies are found by package import. 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.
- Global singletons — the web server layer uses package-level globals (
- Evidence:
server/web/server.go:init()createsBeeApp = NewHttpSever()— a global singleton wired at package load time.core/bean/providesRegisterBean,GetBean, andAutoWire(struct tag–based field injection via reflection).
- Assessment: The dual approach reflects beego’s evolution. The singleton pattern is the heritage from v1;
core/beanis 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(¶ms)
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: embedCache, 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.