Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Gin Administrator & Developer Guide

Generated April 2026 from gin-gonic/gin at d3ffc99

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is for the operator running a Gin-based service in production, the architect integrating Gin into a larger system, and the contributor working on the framework itself. Every claim below is derived from the Gin source at commit d3ffc99 (release v1.12.0).

If you're looking for "how to write a handler", start with the Gin User Guide instead. This document focuses on architecture, configuration surface, deployment patterns, performance, security, internals, and integration points.


1. Architecture Overview

Gin is a single-process HTTP framework. It composes Go's standard net/http server with three custom layers: a radix-tree multiplexer, a middleware chain abstraction, and a request-bound Context that wraps the http.ResponseWriter and exposes ergonomic helpers.

The framework intentionally has no built-in process supervisor, no out-of-process state, and no opinion about where you store session data, cache responses, or speak to a database. It is a stand-alone library you embed inside a Go binary.

1.1 High-level diagram

                 ┌────────────────────────────────────────────────────┐
                 │                  *gin.Engine                       │
 incoming        │                                                    │
 *http.Request → │  ServeHTTP(w, r):                                  │
                 │   1. routeTreesUpdated.Do(updateRouteTrees)        │
                 │   2. c := pool.Get().(*Context)  // sync.Pool      │
                 │   3. c.writermem.reset(w); c.Request = r           │
                 │   4. handleHTTPRequest(c)                          │
                 │      ┌───────────────────────────────────────┐     │
                 │      │  methodTrees (radix tree per method)  │     │
                 │      │  ────────────────────────────────────  │     │
                 │      │  GET  → root *node                     │     │
                 │      │  POST → root *node                     │     │
                 │      │   ...                                   │    │
                 │      │  root.getValue(path, params, ...)      │     │
                 │      └────────────────┬──────────────────────┘     │
                 │                        ▼                            │
                 │               handlers HandlersChain                │
                 │                        ▼                            │
                 │           ┌────────────────────────┐                │
                 │           │ Logger() → Recovery()  │ middleware     │
                 │           │      → user middleware │ chain          │
                 │           │      → final handler   │                │
                 │           └────────────────────────┘                │
                 │                        ▼                            │
                 │              c.Writer.Write(...)                    │
                 │   5. pool.Put(c)                                    │
                 └────────────────────────────────────────────────────┘

1.2 Component inventory

Component Path Role
Engine gin.go The framework instance. Embeds RouterGroup, owns route trees, config flags, sync.Pool of contexts.
RouterGroup routergroup.go Path/middleware prefix builder. The engine itself is a root group.
IRouter / IRoutes routergroup.go Interfaces every group satisfies.
Context context.go Per-request handle. Pooled. Carries Params, Errors, Keys, Writer, etc.
HandlerFunc / HandlersChain gin.go func(*Context) and []HandlerFunc. The chain plus an index is the middleware machinery.
node / methodTree tree.go Radix-tree implementation derived from julienschmidt/httprouter.
responseWriter response_writer.go Wraps http.ResponseWriter, tracks status/size, supports flush/hijack/push.
Error / ErrorType errors.go Tagged error attached to context with c.Error().
binding package binding/ Pluggable Binding/BindingBody/BindingUri implementations for every supported MIME.
render package render/ One Render implementation per MIME (JSON, HTML, XML, BSON, MsgPack, PDF, …).
codec/json codec/json/ Pluggable JSON encoder: stdlib (json.go), goccy/go-json, json-iterator, bytedance/sonic.
internal/bytesconv internal/bytesconv/ Zero-copy []byte ⇄ string helpers.
internal/fs internal/fs/ Thin http.FileSystem wrapper used by template loaders.
ginS ginS/gins.go Optional process-wide singleton wrapper around the package functions.
Logger / LoggerWithConfig logger.go Access-log middleware; ANSI-colored when stdout is a TTY.
Recovery / CustomRecovery recovery.go Panic-catching middleware.
BasicAuth* auth.go Constant-time HTTP Basic and Proxy-Auth middleware.

1.3 Technology stack

Layer Choice Why
Language Go 1.25+ Stdlib-friendly; native net/http integration.
HTTP server net/http Battle-tested. Gin is an http.Handler.
HTTP/2 golang.org/x/net/http2 (+ h2c) Automatically used by ListenAndServeTLS; UseH2C enables cleartext h2.
HTTP/3 / QUIC quic-go/quic-go v0.59.0 Used by RunQUIC.
Routing Custom radix tree Derived from julienschmidt/httprouter (license preserved on every file in tree.go).
Validation go-playground/validator/v10 v10.30.1 Tag-driven; tag name overridden to binding:.
JSON (default) encoding/json Pluggable via codec/json interface.
JSON (alt) bytedance/sonic v1.15.0, goccy/go-json v0.10.6, json-iterator/go v1.1.12 Selected via build tag.
YAML goccy/go-yaml v1.19.2 Replaces stdlib YAML (which doesn't exist) and gopkg.in/yaml.v3.
TOML pelletier/go-toml/v2 v2.2.4 Parsing + rendering.
Protobuf google.golang.org/protobuf v1.36.11 proto.Marshal/Unmarshal.
MsgPack ugorji/go/codec v1.3.1 Encoded only when nomsgpack build tag is not set.
BSON mongo-driver/v2 v2.5.0 Used by both binding.BSON and render.BSON.
SSE gin-contrib/sse v1.1.0 Used by c.SSEvent.
Terminal detect mattn/go-isatty v0.0.20 Decides ANSI color in Logger.
Reflection helper modern-go/reflect2 v1.0.2 Indirect via json-iterator.

1.4 Data flow for a single request

  1. Engine.ServeHTTP is invoked by http.Server.
  2. routeTreesUpdated.Do (a sync.Once) runs updateRouteTrees, replacing the placeholder \: escape with : in every node path. (One-time fixup that lets users register /literal\:colon paths.)
  3. A *Context is fetched from engine.pool (sync.Pool). The pool's factory pre-allocates params and skippedNodes slices sized by engine.maxParams / engine.maxSections, so steady-state per-request allocation is near zero.
  4. c.writermem.reset(w) rewires the response writer wrapper.
  5. c.reset() clears per-request state.
  6. handleHTTPRequest(c) looks up the method's tree, calls root.getValue(rPath, c.params, c.skippedNodes, unescape), populates c.Params and c.handlers, and invokes c.Next() which walks the handler chain.
  7. After all handlers return, pool.Put(c) returns the context to the pool.

1.5 Concurrency model

  • The radix-tree mutates only during route registration. All engine.GET, engine.Group, group.Static, etc., must be called before engine.Run (or whichever Run* you choose).
  • After the first ServeHTTP, routeTreesUpdated.Do(updateRouteTrees) fixes up escaped colons and the trees become read-only for the lifetime of the engine.
  • Each request gets its own *Context (pooled). The pool ensures that goroutine A's context cannot leak into goroutine B's request unless you explicitly call c.Copy() to detach it.
  • Context.Set/Get/MustGet/Delete use a sync.RWMutex (Context.mu) to make Keys map access safe for goroutines spawned from a handler.

Note

The radix-tree commits to the assumption that route registration is single-threaded. Calling r.GET from multiple goroutines while the server is already accepting connections is undefined behavior — never documented as supported, and the comment in tree.go:addRoute says so: "Not concurrency-safe!"

1.6 Filesystem layout

gin/
├── auth.go               BasicAuth + BasicAuthForProxy + Accounts
├── context.go            *Context type and ~100 helper methods
├── debug.go              IsDebugging, debugPrint*
├── doc.go                package-level documentation only
├── errors.go             Error, ErrorType bitmask, errorMsgs
├── fs.go                 OnlyFilesFS / Dir helpers
├── gin.go                Engine, New, Default, Run*, ServeHTTP
├── logger.go             Logger middleware + LogFormatterParams
├── mode.go               Debug/Release/Test mode + GIN_MODE handling
├── path.go               cleanPath / fixedPath helpers
├── recovery.go           Recovery / CustomRecovery middleware
├── response_writer.go    *responseWriter (status, size, hijack, push, flush)
├── routergroup.go        IRouter, IRoutes, RouterGroup, Group/Use/Static…
├── tree.go               radix-tree node, addRoute, getValue
├── utils.go              H, Bind helper, WrapF/WrapH, joinPaths
├── version.go            const Version = "v1.12.0"
├── binding/              one file per content type + form_mapping helpers
├── render/               one file per render type
├── codec/json/           pluggable JSON API surface
├── internal/bytesconv/   string<->[]byte fast paths
├── internal/fs/          http.FileSystem wrapper for templates
└── ginS/                 thin singleton wrapper

1.7 What Gin is NOT

  • Not a process supervisor. No restart, no health-check endpoint unless you write one, no metrics endpoint.
  • Not an ORM. Gin holds opinions about HTTP, not databases.
  • Not a CSRF / session library. Use community middleware (gin-contrib) or roll your own; Gin only provides cookie helpers and same-site enums via c.SetSameSite.
  • Not a clustered runtime. A single Gin process binds a single network address (or socket / fd / QUIC listener). Run multiple replicas behind a load balancer for horizontal scale.

2. System Requirements

2.1 Build environment

Item Requirement
Go toolchain 1.25.0 or newer (declared by go.mod)
Module mode Required (GO111MODULE=on or implicit since Go 1.16)
CPU Any Go-supported architecture (amd64, arm64, ppc64le, s390x, 386, riscv64)
Memory A few hundred MB during go build
Disk ~50 MB module cache including indirect dependencies
Network Egress to proxy.golang.org and sum.golang.org for fetch + checksum

2.2 Runtime environment

Item Notes
OS Linux, macOS, Windows, *BSD, illumos — any Go-supported target. Some Run* methods (e.g., RunUnix, RunFd) are POSIX-only.
File descriptors Default 1024 limit on Linux is fine for moderate traffic; raise via ulimit -n for >1k concurrent connections.
TCP At least one outbound or inbound interface. Run binds 0.0.0.0 by default.
TLS material For RunTLS / RunQUIC: a PEM-encoded cert + private key pair. Test material lives in testdata/certificate/{cert,key}.pem (do not use in production).
Templates If using LoadHTMLGlob, the template files must be readable by the process at runtime.

2.3 Optional dependencies

These are pulled in transitively only if you use the relevant feature:

  • bytedance/sonic — only when JSON codec is set to sonic.
  • quic-go/quic-go + quic-go/qpack — only when RunQUIC is called.
  • mongo-driver/v2 — only when binding or rendering BSON.

3. Configuration Reference

This section is the authoritative list of every public configuration knob, its default, its source, and its effect.

3.1 Engine fields (gin.Engine)

Field Type Default Source Effect
RedirectTrailingSlash bool true gin.go Auto-redirect missing/extra trailing slash to canonical form.
RedirectFixedPath bool false gin.go Try case-insensitive match after cleaning path.
HandleMethodNotAllowed bool false gin.go Return 405 with Allow: header when path matches but method doesn't.
ForwardedByClientIP bool true gin.go If true and remote IP is in trusted-proxy CIDRs, parse RemoteIPHeaders.
AppEngine bool false gin.go Deprecated. Use TrustedPlatform = PlatformGoogleAppEngine.
UseRawPath bool false gin.go Match against URL.RawPath.
UseEscapedPath bool false gin.go Match against URL.EscapedPath(); overrides UseRawPath.
UnescapePathValues bool true gin.go Unescape extracted parameter values.
RemoveExtraSlash bool false gin.go Run path.Clean on request path before lookup.
RemoteIPHeaders []string ["X-Forwarded-For", "X-Real-IP"] gin.go Headers consulted to compute c.ClientIP().
TrustedPlatform string "" gin.go Set to PlatformGoogleAppEngine / PlatformCloudflare / PlatformFlyIO to trust that platform's IP header.
MaxMultipartMemory int64 32 << 20 (32 MB) gin.go Buffer size for multipart parsing; remainder spills to disk.
UseH2C bool false gin.go Wrap with h2c.NewHandler so plain HTTP serves HTTP/2 cleartext.
ContextWithFallback bool false gin.go c.Deadline/Done/Err/Value fall through to c.Request.Context() if local Keys lacks the value.
HTMLRender render.HTMLRender nil gin.go Set by LoadHTMLGlob/LoadHTMLFiles/LoadHTMLFS/SetHTMLTemplate.
FuncMap template.FuncMap {} gin.go Funcs available in HTML templates.

Trusted platform constants:

Constant Header trusted
gin.PlatformGoogleAppEngine X-Appengine-Remote-Addr
gin.PlatformCloudflare CF-Connecting-IP
gin.PlatformFlyIO Fly-Client-IP

Important

When TrustedPlatform is set to a non-empty value, Gin uses that single header as the canonical client IP and skips both RemoteIPHeaders and the trusted-proxy check. Use it only when you are confident that the platform-injected header cannot be spoofed by an external caller.

3.2 Mode constants (mode.go)

Constant Value Set by
gin.DebugMode "debug" Default; auto-selected when no GIN_MODE and no test flag.
gin.ReleaseMode "release" gin.SetMode(gin.ReleaseMode) or GIN_MODE=release.
gin.TestMode "test" Auto-selected when flag.Lookup("test.v") is non-nil; or GIN_MODE=test.
gin.EnvGinMode "GIN_MODE" The env var name itself.

The package-level init() in mode.go reads os.Getenv("GIN_MODE") and calls SetMode, panicking with "gin mode unknown: <value>" on any non-recognized value.

3.3 Other tunable globals

Symbol Default Source Purpose
gin.DefaultWriter os.Stdout mode.go Where Logger() and debug prints go.
gin.DefaultErrorWriter os.Stderr mode.go Where Recovery() and debug-error prints go.
gin.DebugPrintRouteFunc nil debug.go Hook for custom route-print formatting.
gin.DebugPrintFunc nil debug.go Hook for custom debug-print formatting.
binding.Validator *defaultValidator binding/binding.go Validation engine. Replace with your own StructValidator to swap entirely.
binding.EnableDecoderUseNumber false binding/json.go Decode JSON numbers as json.Number (not float64).
binding.EnableDecoderDisallowUnknownFields false binding/json.go Reject unknown JSON fields.

Setters that flip these from gin-package land:

gin.DisableBindValidation()                  // Validator = nil
gin.EnableJsonDecoderUseNumber()             // EnableDecoderUseNumber = true
gin.EnableJsonDecoderDisallowUnknownFields() // EnableDecoderDisallowUnknownFields = true

3.4 Environment variables

Variable Read by Effect
GIN_MODE mode.go:init Sets the engine mode at process start. Values: debug, release, test.
PORT utils.go:resolveAddress Selected by r.Run() when no address is supplied.
TERM logger.go:LoggerWithConfig If dumb, suppresses ANSI color in the access log.

Important

Both GIN_MODE and PORT are the only environment variables Gin reads directly. Everything else is owned by your application; Gin never reads a config file from disk and never writes to one.

3.5 LoggerConfig

gin.LoggerWithConfig(LoggerConfig{...}) accepts:

Field Type Default Purpose
Formatter LogFormatter defaultLogFormatter Function returning the line to write.
Output io.Writer gin.DefaultWriter Where to write.
SkipPaths []string nil Skip lines for these full request paths.
SkipQueryString bool false Strip the query string from the logged path.
Skip Skipper (func(*Context) bool) nil Per-request skip predicate.

LogFormatterParams exposes: Request, TimeStamp, StatusCode, Latency, ClientIP, Method, Path, ErrorMessage, BodySize, Keys. Color helpers: StatusCodeColor, LatencyColor, MethodColor, ResetColor, IsOutputColor.

Color toggles:

gin.DisableConsoleColor() // never colorize
gin.ForceConsoleColor()   // always colorize

By default the formatter colorizes only when gin.DefaultWriter is a TTY and TERM != "dumb".

3.6 RecoveryFunc

gin.CustomRecovery(handle RecoveryFunc) accepts a func(c *Context, err any) that runs after a panic is caught (and after the broken-pipe short-circuit fires). The defaultHandleRecovery simply calls c.AbortWithStatus(http.StatusInternalServerError).

3.7 Functional options (OptionFunc)

gin.New(opts...) and gin.Default(opts...) apply each OptionFunc to the new engine before returning. The pattern:

func WithMaxUploadMB(n int64) gin.OptionFunc {
    return func(e *gin.Engine) { e.MaxMultipartMemory = n << 20 }
}

r := gin.Default(WithMaxUploadMB(8))

engine.With(opts...) is the same operation on an existing engine.

3.8 Configuration files

There are none. Gin does not parse YAML, TOML, JSON, or ENV files itself. Use viper, koanf, os.Getenv, or whatever your application prefers, and apply values to engine fields before Run.


4. Deployment & Scaling

4.1 Deployment topology

A typical production layout:

                 ┌────────────┐
   Internet  ──→ │  Load LB   │ (NGINX, HAProxy, ALB)
                 │  TLS term  │
                 └─────┬──────┘
                       │ HTTP/1.1 or h2c
        ┌──────────────┼──────────────┐
        ▼              ▼              ▼
   ┌────────┐     ┌────────┐     ┌────────┐
   │ gin #1 │     │ gin #2 │     │ gin #3 │  (replica set)
   └────────┘     └────────┘     └────────┘

The Gin process is stateless. Multiple replicas + sticky-by-cookie or round-robin LB scaling is the canonical pattern.

4.2 Listening modes

Mode When to use
r.Run(":8080") Most common. Simple HTTP listener; LB does TLS.
r.RunTLS(":443", cert, key) Direct HTTPS termination (small-to-medium scale).
r.RunUnix("/run/app.sock") Single-host setups; nginx talks via Unix socket for lower overhead.
r.RunFd(fd) systemd socket activation (SystemdSockets() from coreos/go-systemd, then RunFd(int(socketFile.Fd()))).
r.RunListener(l) Pre-configured listener (e.g., tls.NewListener with custom config).
r.RunQUIC(":443", cert, key) HTTP/3 endpoint. Pair with HTTP/2 fallback on the LB.
r.UseH2C = true; r.Run(":8080") HTTP/2 cleartext for service-to-service traffic.

4.3 Build the binary

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
  go build -trimpath -ldflags="-s -w" -o myservice ./cmd/myservice

Stripping symbols (-s -w) cuts a significant fraction of the binary size; -trimpath removes local file paths from the binary for reproducible builds.

For nomsgpack:

go build -tags nomsgpack -trimpath -ldflags="-s -w" -o myservice ./cmd/myservice

4.4 Sample Dockerfile

FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app

FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/app /app
EXPOSE 8080
ENV GIN_MODE=release
ENTRYPOINT ["/app"]

Key choices:

  • distroless/static:nonroot — minimal attack surface; no shell.
  • GIN_MODE=release baked in.
  • CGO_ENABLED=0 so the binary is statically linked and matches the distroless static base.

4.5 Sample systemd unit

[Unit]
Description=My Gin Service
After=network.target

[Service]
Type=simple
User=app
Environment=GIN_MODE=release
Environment=PORT=8080
ExecStart=/usr/local/bin/myservice
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

For socket activation, replace Type=simple with Type=notify and add a .socket unit that hands a file descriptor; consume with RunFd.

4.6 Horizontal scaling notes

  • Statelessness: Don't store anything in c.Keys that you expect to survive across requests. Use Redis/Postgres/etc.
  • Sticky sessions: Only required if you cache user state in process memory (don't). If you must, configure your LB to pin by cookie or source IP.
  • Health check: Add a r.GET("/healthz", ...) returning 200; point your LB and orchestrator at it.
  • Graceful shutdown: r.Run has no Shutdown. Use *http.Server directly (User Guide §5.10) and listen for SIGTERM.

Tip

When running behind Kubernetes, set terminationGracePeriodSeconds to exceed your in-flight request timeout, and configure a preStop hook that sleeps just long enough for the LB to drain the pod before SIGTERM lands.

4.7 Vertical sizing

  • CPU: Gin is CPU-bound on JSON encode/decode and on regex/path cleanup for RedirectFixedPath. A modern x86 core handles tens of thousands of requests per second on small payloads.
  • Memory: Per-request memory is dominated by the request body. The context pool keeps allocations tight; expect a few KB of headroom per in-flight request.
  • GOMAXPROCS: Set to the number of cores allocated to the container (e.g., via automaxprocs or by reading the cgroup quota). Defaults to the host's CPU count, which over-allocates inside containers.

5. Operations

5.1 Logs

Gin writes two logical streams:

  • Access log — one line per request via Logger(), to gin.DefaultWriter (stdout by default).
  • Error / panic log — Recovery() panics + debugPrint* warnings, to gin.DefaultErrorWriter (stderr).

The default access-log format:

[GIN] 2026/04/28 - 14:50:00 | 200 |   1.234ms |  10.0.0.5 | GET     "/api/v1/users/42"

Parsed positionally. For structured logs, swap the formatter:

r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
    Formatter: func(p gin.LogFormatterParams) string {
        b, _ := json.Marshal(map[string]any{
            "ts":     p.TimeStamp.UTC().Format(time.RFC3339Nano),
            "status": p.StatusCode,
            "lat_ms": p.Latency.Milliseconds(),
            "ip":     p.ClientIP,
            "method": p.Method,
            "path":   p.Path,
        })
        return string(b) + "\n"
    },
    Output: os.Stdout,
}))

5.2 Health checks

There is no built-in health endpoint. Add one:

r.GET("/healthz", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"status": "ok", "version": gin.Version})
})

r.GET("/readyz", func(c *gin.Context) {
    if !db.Ping() {
        c.AbortWithStatusJSON(503, gin.H{"db": "down"})
        return
    }
    c.JSON(200, gin.H{"status": "ready"})
})

Note

Distinguish liveness (process is up) from readiness (process is capable of serving traffic). /healthz should always succeed if the process is running; /readyz should fail when downstream dependencies are unhealthy so orchestrators stop sending traffic.

5.3 Metrics

No built-in metrics. Pair with prometheus/client_golang and a middleware:

import "github.com/prometheus/client_golang/prometheus"
import "github.com/prometheus/client_golang/prometheus/promhttp"

requestDuration := prometheus.NewHistogramVec(/* ... */)
prometheus.MustRegister(requestDuration)

r.Use(func(c *gin.Context) {
    start := time.Now()
    c.Next()
    requestDuration.WithLabelValues(
        c.Request.Method,
        c.FullPath(),                    // route pattern, not raw URL
        strconv.Itoa(c.Writer.Status()),
    ).Observe(time.Since(start).Seconds())
})

r.GET("/metrics", gin.WrapH(promhttp.Handler()))

c.FullPath() is the right label — it returns /users/:id, not /users/42, so cardinality stays bounded.

5.4 Tracing

OpenTelemetry integrates via middleware that creates a span around each request. The pattern:

r.Use(func(c *gin.Context) {
    ctx, span := tracer.Start(c.Request.Context(), c.FullPath())
    defer span.End()
    c.Request = c.Request.WithContext(ctx)
    c.Next()
    span.SetAttributes(
        attribute.Int("http.status_code", c.Writer.Status()),
        attribute.String("http.method", c.Request.Method),
    )
})

Set r.ContextWithFallback = true so the trace context propagates through c.Deadline/Done/Err/Value.

5.5 Common runbooks

Process pegged at 100% CPU

  1. Capture a profile: go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30 (assumes you've registered the net/http/pprof package with gin.WrapH).
  2. Look for hot functions in JSON encoding (encoding/json.Marshal), sync.(*Mutex).Lock, or template execution.
  3. Check whether gin.IndentedJSON is used in hot paths — it is roughly 10x slower than gin.JSON; replace with gin.JSON if so.

Process leaking memory

  1. Capture a heap profile: go tool pprof http://localhost:8080/debug/pprof/heap.
  2. Check c.Set usage — if you store unbounded data in c.Keys and copy contexts into a goroutine via c.Copy(), that data outlives the request.
  3. Inspect template glob — if LoadHTMLGlob is called on every request (it shouldn't be), each call re-parses templates and leaks closures.

Connections refused / stuck pending

  1. ss -ltn | grep <port> — confirm the listener is open.
  2. Check ulimit -n and the listener backlog (set via net.Listen if you use RunListener).
  3. Look for handlers blocking forever. The Recovery middleware does not time out long-running handlers; you must add a request timeout middleware yourself or set WriteTimeout / ReadTimeout on the underlying *http.Server (use RunListener to install them).

Tip

Use http.TimeoutHandler(handler, dur, msg) wrapped via gin.WrapH(...) for per-request budgets. It returns 503 if the handler exceeds the deadline, but ungracefully — the goroutine continues running.

5.6 Backup & state

Gin holds no persistent state. Backup considerations are entirely about the data store you use. The only on-disk files Gin touches are the ones your handlers create (uploads via SaveUploadedFile, templates loaded from disk).


6. Performance

6.1 Throughput characteristics

The benchmark suite (benchmarks_test.go) measures the steady-state cost of hitting an empty handler. Representative results on commodity x86:

  • BenchmarkOneRoute — serving a single registered route with no middleware. Lowest possible cost.
  • BenchmarkRecoveryMiddleware — adds the cost of a deferred recover() per request.
  • BenchmarkLoggerMiddleware — adds the cost of formatting one log line to a mockWriter (no I/O to the kernel).
  • BenchmarkManyHandlers — adds three trivial middleware to confirm the loop is not super-linear.
  • Benchmark5Params — five path parameters; tests the radix-tree walking + parameter capture cost.
  • BenchmarkOneRouteJSON, BenchmarkOneRouteHTML, BenchmarkOneRouteString — measure renderer cost separately.
  • Benchmark404, Benchmark404Many — measure the not-found path, including the "trailing slash" / "fixed path" probes.

These confirm Gin's allocation budget per request is bounded and small. Reproduce locally:

go test -bench=. -benchmem -count=5 ./... | tee bench.txt

6.2 Hot-path optimizations Gin uses

  • Context pooling. engine.pool.New allocates a *Context with pre-sized Params and skippedNodes slices. After the first warmup, pool.Get/Put is allocation-free in steady state.
  • Zero-copy strings. internal/bytesconv.{StringToBytes,BytesToString} use unsafe to alias []byte and string headers without copy. Used in basic-auth comparison (auth.go), path canonicalization (tree.go), and JSONP bookkeeping.
  • Radix tree. The router avoids regex; matching is byte-by-byte longest-common-prefix. Each level keeps children sorted by hit count (incrementChildPrio) so frequently used routes are reached first.
  • Sync.Once for tree finalization. routeTreesUpdated.Do runs the one-time path fixup on the first request rather than at registration, so registration is fast.
  • Lazy query/form caches. c.queryCache and c.formCache are populated on first access and reused for subsequent reads.
  • Pluggable JSON. codec/json/api.go exposes a JSONAPI interface; drop-in replacements for encoding/json are available via build tags (sonic, go-json, jsoniter).

6.3 Tuning levers

Lever When to use
Build with -tags sonic (or your JSON of choice) Hot JSON paths > 30% of CPU time.
r.MaxMultipartMemory = small If you handle small multipart bodies, lower this so memory pressure stays low.
Replace Logger() with custom formatter If defaultLogFormatter shows up in profiles, swap to a non-colorized minimal formatter.
gin.DisableConsoleColor() Silences ANSI escape codes (small but consistent saving in non-TTY logs).
Custom validator The default validator caches metadata per type via sync.Once; first request per type pays the reflection cost.
Reduce middleware depth Each middleware is a function call + c.index increment. Keep the global stack short.

Tip

Always measure before optimizing. Run pprof and confirm the bottleneck before swapping JSON codecs or rewriting middleware. The default encoding/json is the path of least surprise and adequate for most workloads.

6.4 Anti-patterns

  • gin.IndentedJSON in hot paths. ~10x slower than gin.JSON. Use for debug endpoints only.
  • SetHTMLTemplate after server start. The function is not thread safe (debug.go:debugPrintWARNINGSetHTMLTemplate warns explicitly). Call it before Run.
  • Reading c.Request.Body in a middleware without buffering. Once read, it's gone. Use ShouldBindBodyWith* to make the body re-readable.
  • c.Copy() in every request without need. It clones Keys and Params; the allocation is real. Only use it when handing the context to a goroutine that outlives the request.

7. Security

Gin is a thin HTTP framework — security responsibilities are split between the framework, the operator, and the application code. This section catalogs the framework's surface and identifies places where you must add your own controls.

7.1 What Gin handles for you

  • Constant-time Basic Auth comparison. auth.go:searchCredential uses crypto/subtle.ConstantTimeCompare against the full Basic base64(user:pass) value, mitigating timing attacks.
  • HTML auto-escaping in templates. Because Gin uses html/template (not text/template) for LoadHTMLGlob, contextual auto-escaping applies — {{.UntrustedInput}} is escaped by template type.
  • Authorization-header masking in panic dumps. recovery.go:secureRequestDump rewrites any Authorization: line in the panic dump to Authorization: *. Other headers are unmodified.
  • SecureJSON array-prefix protection. When the top-level value is a JSON array, c.SecureJSON prefixes the output with the configurable secureJSONPrefix (default while(1);). This frustrates pre-2014 JSON hijacking attacks. Configure via engine.SecureJsonPrefix(prefix).
  • Path canonicalization. RemoveExtraSlash and RedirectFixedPath reduce the surface for path-traversal-style routing tricks.
  • Trusted-proxies enforcement. c.ClientIP() only reads RemoteIPHeaders if the connecting peer is in trustedCIDRs. The default is 0.0.0.0/0 + ::/0 (i.e., trust everyone) — see §7.4.

7.2 What Gin does NOT handle

  • CSRF protection. No built-in CSRF token middleware. Use a community package (e.g., gin-contrib/csrf) or implement double-submit cookies yourself.
  • Rate limiting. No built-in. Use golang.org/x/time/rate or a proxy-level limiter.
  • Content Security Policy / Strict-Transport-Security. Set these yourself with c.Header(...) or a community middleware.
  • CORS. No built-in. gin-contrib/cors is the canonical choice.
  • Body size cap. MaxMultipartMemory only governs multipart parsing. For total body size, wrap with http.MaxBytesReader in middleware.
  • Slow-loris / read-timeout. Gin uses &http.Server{Handler: ...} with no read or write timeout. Set them via RunListener or construct the http.Server yourself.
  • Encryption at rest / TLS material rotation. Operational concerns outside the framework.

Caution

The default r.Run-built http.Server has zero read, write, and idle timeouts. A single misbehaving client (or an attacker holding a connection open with a slow trickle of bytes) can keep a goroutine alive indefinitely. Always construct *http.Server yourself, set ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout, then call r.RunListener or pass r.Handler() to your server.

7.3 TLS / HTTPS

r.RunTLS(addr, certFile, keyFile) wraps http.ListenAndServeTLS. The underlying tls.Config is the Go default — modern ciphers, TLS 1.2+, ALPN for HTTP/2.

To customize TLS (e.g., minimum version, cipher suites, ALPN, mutual TLS):

listener, err := tls.Listen("tcp", ":443", &tls.Config{
    MinVersion: tls.VersionTLS12,
    Certificates: []tls.Certificate{cert},
    // ClientCAs / ClientAuth for mTLS, etc.
})
if err != nil { log.Fatal(err) }
r.RunListener(listener)

For QUIC/HTTP/3, r.RunQUIC accepts a cert + key; the handshake is HTTP/3-only. Combine with HTTP/2 fallback at the LB if some clients are HTTP/2-only.

7.4 Trusted proxies (the most-misunderstood setting)

By default Gin trusts every proxy: trustedProxies = ["0.0.0.0/0", "::/0"]. Combined with ForwardedByClientIP = true and RemoteIPHeaders = ["X-Forwarded-For", "X-Real-IP"], this means any external client can supply an X-Forwarded-For header and Gin will believe it.

engine.isUnsafeTrustedProxies returns true for this default configuration; the Run/RunTLS/RunUnix/RunFd/RunQUIC/RunListener methods all log a [WARNING] You trusted all proxies, this is NOT safe debug print on startup.

Hardening:

// Trust only the LB CIDR
r.SetTrustedProxies([]string{"10.0.0.0/8"})

// OR: disable header parsing entirely
r.SetTrustedProxies(nil)

When SetTrustedProxies(nil) is in effect, c.ClientIP() returns the TCP peer address only. That's the right answer when Gin is the edge.

Warning

If you log, rate-limit, ban, or audit on c.ClientIP(), you are effectively trusting whatever X-Forwarded-For says — and any external client can spoof that. Lock down SetTrustedProxies before the first production deployment.

7.5 Authentication patterns

Gin ships BasicAuth and BasicAuthForProxy. Both compare in constant time. Neither implements rate limiting or account lockout — add those at the application layer.

For token / OAuth / JWT auth, write a middleware:

func RequireToken() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if !strings.HasPrefix(token, "Bearer ") {
            c.AbortWithStatusJSON(401, gin.H{"error": "missing bearer token"})
            return
        }
        claims, err := verify(strings.TrimPrefix(token, "Bearer "))
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
            return
        }
        c.Set("claims", claims)
        c.Next()
    }
}

AbortWithStatusJSON ensures handlers below the middleware never run.

c.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) constructs a single http.Cookie. The secure and httpOnly flags are required for session cookies; c.SetSameSite(http.SameSiteStrictMode) (or Lax) before SetCookie adds the SameSite attribute.

c.SetCookieData(*http.Cookie) is the lower-level escape hatch for setting all cookie fields (including MaxAge=-1 to delete, Partitioned, Quoted, etc.).

7.7 Header sanitization

Gin does not sanitize response headers — c.Header(k, v) is a literal write. Reject newlines from user-controlled input before passing it to Header to prevent header-injection.

7.8 Recovery panic handling

Recovery() writes a 500 response with no body and prints the panic + stack trace to stderr. In debug mode the dump is verbose (request body + headers + stack); in release mode only the panic value + stack are printed. The Authorization: header is masked in both modes (secureRequestDump).

Caution

Other sensitive headers (Cookie, X-API-Key, etc.) are not masked by Recovery. If you log panics to a centralized system, consider wrapping CustomRecoveryWithWriter to scrub headers before they hit your log pipeline.

7.9 Dependency security

Gin's go.mod pins major versions of cryptographic and parsing libraries. At commit d3ffc99:

  • github.com/go-playground/validator/v10 v10.30.1
  • golang.org/x/crypto v0.49.0
  • golang.org/x/net v0.52.0
  • github.com/quic-go/quic-go v0.59.0

Run go list -m -u all periodically to spot upstream advisories, and keep golang.org/x/{crypto,net} close to upstream — security fixes ship there first.

Issue #4236 (CLOSED) tracked GO-2025-3563; the dependency was upgraded.


8. Developer Internals

This section is for contributors and integrators who need to know how Gin works at the source level.

8.1 The radix tree (tree.go)

The router is a per-method radix tree. Each methodTree holds a root *node; engine.trees is a slice of them. engine.addRoute(method, path, handlers) finds (or creates) the tree for the method and calls root.addRoute(path, handlers).

Node anatomy:

type node struct {
    path      string         // prefix matched at this node
    indices   string          // first byte of each child path; index by byte search
    wildChild bool            // true if the last child is a :param or *catchAll
    nType     nodeType        // static / root / param / catchAll
    priority  uint32          // hit count; used for re-ordering siblings
    children  []*node         // sorted by priority (most-hit first)
    handlers  HandlersChain   // the chain to invoke when this leaf matches
    fullPath  string          // for c.FullPath()
}

Insertion (addRoute) walks the tree byte-by-byte using longestCommonPrefix. When a new path diverges, the node splits: a child is inserted carrying the old path tail, and a new branch carries the new path tail. Wildcards (:name, *name) become param or catchAll nodes.

Lookup (getValue) walks the same byte-by-byte path. When it hits a param node, it captures the corresponding URL segment into the Params slice; for catchAll it captures the entire remaining path.

Performance properties:

  • O(L) where L is the URL path length. No regex, no backtracking (modulo a small skippedNodes stack used for wildcard fallback).
  • Re-ordering siblings. Each successful match calls incrementChildPrio to bump the child's priority and reorder it toward the front, so frequently used routes are matched first.
  • Cap on parameters. engine.maxParams and engine.maxSections track the largest path's parameter / /-section count and drive the pre-sized Params slice in the pool factory.

8.2 The middleware chain (HandlersChain + Context.Next)

HandlersChain is []HandlerFunc. The chain for a route is built at registration:

finalChain = parentGroup.Handlers ++ groupHandlers ++ routeHandler

Context.Next walks c.handlers from c.index+1 to the end, invoking each. Context.Abort sets c.index = abortIndex (a sentinel). After that, Next becomes a no-op.

Two implications:

  1. The chain can be paused at any point by calling c.Next() from inside a middleware. Code before c.Next() runs on the way in; code after runs on the way back. This is how Gin's logger captures the final status code.
  2. The chain can be abandoned by calling c.Abort(). Subsequent middlewares are skipped on the way back as well, because each handler's Next() call sees c.index >= abortIndex and returns immediately.

Final chain length is capped at abortIndex - 1 = 63 total handlers (the assert1(finalSize < int(abortIndex), "too many handlers") check in routergroup.go:combineHandlers).

8.3 Context lifecycle

Engine.ServeHTTP
  ├── pool.Get   → *Context (reused or newly allocated)
  ├── reset      → clear Params, Keys, Errors, Accepted, queryCache, formCache
  ├── handleHTTPRequest
  │     ├── tree lookup → set Params / handlers / fullPath
  │     ├── Next()      → run middleware chain
  │     └── writeHeaderNow if not aborted
  └── pool.Put   → return *Context for reuse

Context.Copy returns a detached copy safe to pass to a goroutine. It shallow-copies engine and clones Keys + Params. The copy's writermem.ResponseWriter is set to nil because writing to the original response after the request returns would race with the pool.

8.4 The binding system (binding/)

Three interfaces:

type Binding interface {
    Name() string
    Bind(*http.Request, any) error
}

type BindingBody interface {
    Binding
    BindBody([]byte, any) error
}

type BindingUri interface {
    Name() string
    BindUri(map[string][]string, any) error
}

Each MIME has a one-line implementer (jsonBinding, xmlBinding, etc.) that decodes the request body (or query/form) into the target value and calls validate(obj) to run struct-tag validation.

binding.Default(method, contentType) selects the right binder: GET → Form, POST application/json → JSON, … application/xml → XML, … multipart/form-data → FormMultipart, … application/toml → TOML, … application/bson → BSON, fallback Form.

Form mapping (binding/form_mapping.go) handles form:"key" tagged structs. It supports primitives, slices, maps, time, custom types via UnmarshalParam/UnmarshalText, and multipart.FileHeader for upload fields. The error "unknown type" (form_mapping.go:23) fires when a field's reflect kind isn't in the supported set.

8.5 The render system (render/)

A single interface:

type Render interface {
    Render(http.ResponseWriter) error
    WriteContentType(http.ResponseWriter)
}

Each MIME is a struct (e.g., JSON{Data: any}, XML{Data: any}, HTML{Template, Name, Data}, BSON{Data}, PDF{Data []byte}). The Context.Render(code, r) method writes the status, calls r.WriteContentType(w), then r.Render(w).

HTML is special because it has two flavors:

  • HTMLProduction{Template, Delims} — pre-parsed, shared template; fast.
  • HTMLDebug{Files | Glob | FileSystem+Patterns, Delims, FuncMap} — reparses templates on every request; used in debug mode.

render.Delims{Left, Right} lets you swap {{/}} for any other delimiter pair (useful when the templates need to be rendered through multiple template engines).

8.6 Pluggable JSON codec (codec/json/)

The package exposes one variable:

var API JSONAPI = jsonApi{}

Where JSONAPI is:

type JSONAPI interface {
    Marshal(any) ([]byte, error)
    Unmarshal([]byte, any) error
    NewEncoder(io.Writer) JSONEncoder
    NewDecoder(io.Reader) JSONDecoder
    MarshalIndent(any, prefix, indent string) ([]byte, error)
    Indent(*bytes.Buffer, []byte, string, string) error
}

The default jsonApi wraps encoding/json. Build tags switch the implementation:

File Build tag Backend
codec/json/json.go (no tag) encoding/json
codec/json/go_json.go go_json goccy/go-json
codec/json/jsoniter.go jsoniter json-iterator/go
codec/json/sonic.go sonic bytedance/sonic

Pick at compile time:

go build -tags sonic ./...

8.7 internal/bytesconv

Two functions perform unsafe []byte ⇄ string aliasing without copying. They're correct as long as the resulting string is treated as read-only and the underlying byte slice is not mutated. Used in performance-critical paths: basic-auth comparison (auth.go:searchCredential), path canonicalization (tree.go), JSONP handling (render/json.go), and panic-dump masking (recovery.go).

8.8 Testing helpers (test_helpers.go)

gin.CreateTestContext(w http.ResponseWriter) returns (*gin.Context, *gin.Engine) so unit tests can construct a context without booting a server. The engine is a gin.Default instance.

The test_helpers.go file also contains the internal helper waitForServerReady(url, attempts) used by integration tests (gin_integration_test.go); it polls the URL with exponential backoff and returns when the server is up.

8.9 Code patterns and conventions

  • Comment style: Every exported symbol is preceded by a Go-doc comment that begins with the symbol name. The framework follows revive and gofumpt formatting (configured in .golangci.yml).
  • Errors: Use errors.New for static text, fmt.Errorf with %w for wrapping. Typed errors are tagged via *gin.Error.Type so middleware can filter by ErrorTypeBind, ErrorTypeRender, ErrorTypePrivate, ErrorTypePublic, ErrorTypeAny.
  • Panics for programmer error. Bad route paths, empty Basic-Auth credentials, invalid HTTP methods, and double-running Run are panics (or returned errors) — never silent failures.
  • No goroutines from the framework. Gin itself never spawns a goroutine. The user's handlers and middleware do.
  • No global mutable state in hot paths. Route trees and pools live on the *Engine value, not in package-level vars (with the exception of ginMode, DefaultWriter, and similar).

8.10 Test layout

gin/
├── *_test.go                  in-package tests for every public file
├── benchmarks_test.go         pkg-level benchmarks
├── gin_integration_test.go    spawns real listeners
├── githubapi_test.go          regression test for routing edge cases
├── routes_test.go             routing-tree exhaustive matrix
├── binding/*_test.go
├── render/*_test.go
└── ginS/gins_test.go

CI runs go test ./... (linted by golangci-lint, configured in .golangci.yml). The Makefile exposes make test, make lint, and make bench targets.

8.11 Known issues and tech debt

  • Issue #4572 (OPEN) — X-Forwarded-For parser doesn't accept RFC 7239 Forwarded: syntax. Workaround documented in User Guide §6.
  • Issue #4622 (OPEN) — SaveUploadedFile chmods existing parent directories on some filesystems. Avoid saving directly into shared directories.
  • Issue #4068 (OPEN) — c.SaveUploadedFile permissions are platform dependent; the perm ...fs.FileMode variadic argument lets you pin the saved file's mode but not the directory's.
  • Issue #4624 (OPEN) — Param binding and validation are coupled in the binding: tag. A future feature request asks for separate hooks.
  • Issue #4493 (CLOSED) — Typed parameters (e.g., :id<int>) are not supported. Bind via c.ShouldBindUri and validator tags instead.
  • Issue #4332 (CLOSED) — There is no Engine.Shutdown. Use *http.Server directly (User Guide §5.10).
  • Issue #3047 (CLOSED) — No way to opt a single route out of an applied middleware. Use route groups instead.

8.12 Contributing

Standard Go workflow:

git clone https://github.com/gin-gonic/gin
cd gin
go test ./...
golangci-lint run

The repo uses GoReleaser (.goreleaser.yaml) for releases — the build section is skip: true because Gin is a library, not a binary.

The .golangci.yml enables revive, gosec, errorlint, misspell, nilerr, nakedret, perfsprint, testifylint, usestdlibvars, and several others. Run golangci-lint run before sending a PR.


9. Integration Points

9.1 Importing as a library

Gin is a Go library and integrates anywhere a Go binary runs. Single-import:

import "github.com/gin-gonic/gin"

Sub-packages:

import (
    "github.com/gin-gonic/gin/binding"
    "github.com/gin-gonic/gin/render"
    "github.com/gin-gonic/gin/codec/json"
)

The ginS package is a stylistic option only — every function delegates to a sync.OnceValue-initialized gin.Default() instance.

9.2 Wrapping standard http.Handler middleware

gin.WrapH(h http.Handler) gin.HandlerFunc and gin.WrapF(f http.HandlerFunc) adapt stdlib middleware. Use them for:

  • prometheus.Handler() (gin.WrapH(promhttp.Handler()))
  • pprof.Handler("...") (gin.WrapH(pprof.Handler("heap")))
  • Any third-party http.Handler you want to mount on a Gin route.

9.3 Mounting Gin inside another router

*gin.Engine implements http.Handler, so you can mount it under http.ServeMux:

mainMux := http.NewServeMux()
mainMux.Handle("/api/", http.StripPrefix("/api", apiEngine))
mainMux.Handle("/", staticEngine)
http.ListenAndServe(":8080", mainMux)

Note that http.StripPrefix is essential when mounting under a prefix — Gin's tree expects URL paths to start at /, not at the mount point.

9.4 Embedding inside a *http.Server

srv := &http.Server{
    Addr:              ":8080",
    Handler:           r.Handler(), // not r directly, so UseH2C is honored
    ReadHeaderTimeout: 10 * time.Second,
    ReadTimeout:       30 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       120 * time.Second,
    TLSConfig:         &tls.Config{MinVersion: tls.VersionTLS12},
}
go srv.ListenAndServeTLS("", "")

This is the recommended path for any production deployment because it gives you total control over timeouts, TLS, and shutdown.

9.5 Gin + reverse proxy

Behind nginx / Traefik / Caddy / HAProxy:

┌─────────┐     ┌─────────┐     ┌─────────┐
│ Client  │ ──→ │ NGINX   │ ──→ │ Gin     │
│         │     │ (TLS,   │     │ (HTTP/1)│
│         │     │  CORS,  │     │         │
│         │     │  Cache) │     │         │
└─────────┘     └─────────┘     └─────────┘

Configuration checklist:

  • Set r.SetTrustedProxies to the LB's CIDR.
  • Have the LB inject X-Forwarded-For and X-Real-IP (Gin reads both by default).
  • Have the LB inject X-Forwarded-Proto if you redirect on scheme.
  • Disable Gin's RedirectTrailingSlash if the LB already canonicalizes.

9.6 Gin + WebSocket

Gin doesn't ship WebSocket support directly, but *gin.Context exposes c.IsWebsocket() (true if the request has the upgrade header) and gives you the underlying c.Writer (which implements http.Hijacker — verified by the integration test TestPusher in gin_integration_test.go). Use gorilla/websocket or nhooyr/websocket:

upgrader := websocket.Upgrader{}
r.GET("/ws", func(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil { return }
    defer conn.Close()
    for {
        _, msg, err := conn.ReadMessage()
        if err != nil { return }
        conn.WriteMessage(websocket.TextMessage, msg)
    }
})

c.Writer.Pusher() exposes the HTTP/2 push interface on TLS connections.

9.7 Gin + Server-Sent Events

The gin-contrib/sse package is imported directly by context.go. c.SSEvent(name, msg) writes a single event; c.Stream(step) is the canonical loop. The event-stream Content-Type is set automatically (render/reader.go).

9.8 Gin + gRPC

Gin and gRPC normally run on separate ports. To multiplex them on one port, use golang.org/x/net/http2/h2c (already a transitive dep when UseH2C is true) and dispatch by Content-Type: application/grpc:

r.UseH2C = true
mux := http.NewServeMux()
mux.Handle("/grpc.MyService/", grpcServer)
mux.Handle("/", r.Handler())
http.ListenAndServe(":8080", mux)

For HTTP/3 + gRPC, see RunQUIC.

9.9 Gin + database/queue/etc.

Gin does not own state. Persist via the database driver of your choice:

  • database/sql for SQL.
  • redis/go-redis for Redis.
  • mongo-driver/v2 for MongoDB (already a Gin dep, used for BSON rendering).
  • lib/pq, pgx, mysql, mongo, etc.

The conventional pattern is to construct your dependencies in main and capture them in handler closures via dependency injection:

func main() {
    db := openDB()
    r := gin.Default()
    r.GET("/users/:id", makeGetUser(db))
    r.Run()
}

func makeGetUser(db *sql.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        // ...
    }
}

9.10 Gin + observability platforms

Platform Integration
Prometheus gin.WrapH(promhttp.Handler()) + per-request histogram middleware. Use c.FullPath() as the route label.
OpenTelemetry Span-creating middleware around c.Next(). Set r.ContextWithFallback = true so context.Context works in handlers.
Datadog / New Relic Same pattern; they ship Gin-specific middleware in their go-agent SDKs.
Jaeger OTel-compatible; same as above.
Sentry sentry-go ships a Gin middleware that wraps panics.

9.11 Versioning and release cadence

Releases follow semver. The Version constant in version.go is the canonical source of truth at runtime. go.mod declares go 1.25.0 — the minimum supported toolchain.

GoReleaser (.goreleaser.yaml) configures the release notes generator; groups commit messages by feat, fix, chore, refactor, build/ci, docs. The release notes appear on the GitHub Releases page.

9.12 Forking notes

The radix-tree implementation in tree.go carries the original julienschmidt/httprouter BSD-style license. Any fork that touches tree.go must preserve the copyright notice. The rest of Gin is MIT licensed.