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
Engine.ServeHTTPis invoked byhttp.Server.routeTreesUpdated.Do(async.Once) runsupdateRouteTrees, replacing the placeholder\:escape with:in every node path. (One-time fixup that lets users register/literal\:colonpaths.)- A
*Contextis fetched fromengine.pool(sync.Pool). The pool's factory pre-allocatesparamsandskippedNodesslices sized byengine.maxParams/engine.maxSections, so steady-state per-request allocation is near zero. c.writermem.reset(w)rewires the response writer wrapper.c.reset()clears per-request state.handleHTTPRequest(c)looks up the method's tree, callsroot.getValue(rPath, c.params, c.skippedNodes, unescape), populatesc.Paramsandc.handlers, and invokesc.Next()which walks the handler chain.- 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 beforeengine.Run(or whicheverRun*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 callc.Copy()to detach it. Context.Set/Get/MustGet/Deleteuse async.RWMutex(Context.mu) to makeKeysmap access safe for goroutines spawned from a handler.
Note
The radix-tree commits to the assumption that route registration is single-threaded. Calling
r.GETfrom multiple goroutines while the server is already accepting connections is undefined behavior — never documented as supported, and the comment intree.go:addRoutesays 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 whenRunQUICis 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
TrustedPlatformis set to a non-empty value, Gin uses that single header as the canonical client IP and skips bothRemoteIPHeadersand 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_MODEandPORTare 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=releasebaked in.CGO_ENABLED=0so the binary is statically linked and matches the distrolessstaticbase.
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.Keysthat 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.Runhas noShutdown. Use*http.Serverdirectly (User Guide §5.10) and listen for SIGTERM.
Tip
When running behind Kubernetes, set
terminationGracePeriodSecondsto exceed your in-flight request timeout, and configure apreStophook 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
automaxprocsor 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(), togin.DefaultWriter(stdout by default). - Error / panic log —
Recovery()panics +debugPrint*warnings, togin.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).
/healthzshould always succeed if the process is running;/readyzshould 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
- Capture a profile:
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30(assumes you've registered thenet/http/pprofpackage withgin.WrapH). - Look for hot functions in JSON encoding (
encoding/json.Marshal),sync.(*Mutex).Lock, or template execution. - Check whether
gin.IndentedJSONis used in hot paths — it is roughly 10x slower thangin.JSON; replace withgin.JSONif so.
Process leaking memory
- Capture a heap profile:
go tool pprof http://localhost:8080/debug/pprof/heap. - Check
c.Setusage — if you store unbounded data inc.Keysand copy contexts into a goroutine viac.Copy(), that data outlives the request. - Inspect template glob — if
LoadHTMLGlobis called on every request (it shouldn't be), each call re-parses templates and leaks closures.
Connections refused / stuck pending
ss -ltn | grep <port>— confirm the listener is open.- Check
ulimit -nand the listener backlog (set vianet.Listenif you useRunListener). - 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/ReadTimeouton the underlying*http.Server(useRunListenerto install them).
Tip
Use
http.TimeoutHandler(handler, dur, msg)wrapped viagin.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 deferredrecover()per request.BenchmarkLoggerMiddleware— adds the cost of formatting one log line to amockWriter(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.Newallocates a*Contextwith pre-sizedParamsandskippedNodesslices. After the first warmup,pool.Get/Putis allocation-free in steady state. - Zero-copy strings.
internal/bytesconv.{StringToBytes,BytesToString}useunsafeto alias[]byteandstringheaders 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.Doruns the one-time path fixup on the first request rather than at registration, so registration is fast. - Lazy query/form caches.
c.queryCacheandc.formCacheare populated on first access and reused for subsequent reads. - Pluggable JSON.
codec/json/api.goexposes aJSONAPIinterface; drop-in replacements forencoding/jsonare 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
pprofand confirm the bottleneck before swapping JSON codecs or rewriting middleware. The defaultencoding/jsonis the path of least surprise and adequate for most workloads.
6.4 Anti-patterns
gin.IndentedJSONin hot paths. ~10x slower thangin.JSON. Use for debug endpoints only.SetHTMLTemplateafter server start. The function is not thread safe (debug.go:debugPrintWARNINGSetHTMLTemplatewarns explicitly). Call it beforeRun.- Reading
c.Request.Bodyin a middleware without buffering. Once read, it's gone. UseShouldBindBodyWith*to make the body re-readable. c.Copy()in every request without need. It clonesKeysandParams; 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:searchCredentialusescrypto/subtle.ConstantTimeCompareagainst the fullBasic base64(user:pass)value, mitigating timing attacks. - HTML auto-escaping in templates. Because Gin uses
html/template(nottext/template) forLoadHTMLGlob, contextual auto-escaping applies —{{.UntrustedInput}}is escaped by template type. - Authorization-header masking in panic dumps.
recovery.go:secureRequestDumprewrites anyAuthorization:line in the panic dump toAuthorization: *. Other headers are unmodified. SecureJSONarray-prefix protection. When the top-level value is a JSON array,c.SecureJSONprefixes the output with the configurablesecureJSONPrefix(defaultwhile(1);). This frustrates pre-2014 JSON hijacking attacks. Configure viaengine.SecureJsonPrefix(prefix).- Path canonicalization.
RemoveExtraSlashandRedirectFixedPathreduce the surface for path-traversal-style routing tricks. - Trusted-proxies enforcement.
c.ClientIP()only readsRemoteIPHeadersif the connecting peer is intrustedCIDRs. The default is0.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/rateor 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/corsis the canonical choice. - Body size cap.
MaxMultipartMemoryonly governs multipart parsing. For total body size, wrap withhttp.MaxBytesReaderin middleware. - Slow-loris / read-timeout. Gin uses
&http.Server{Handler: ...}with no read or write timeout. Set them viaRunListeneror construct thehttp.Serveryourself. - Encryption at rest / TLS material rotation. Operational concerns outside the framework.
Caution
The default
r.Run-builthttp.Serverhas 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.Serveryourself, setReadHeaderTimeout,ReadTimeout,WriteTimeout, andIdleTimeout, then callr.RunListeneror passr.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 whateverX-Forwarded-Forsays — and any external client can spoof that. Lock downSetTrustedProxiesbefore 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.
7.6 Cookie security
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 wrappingCustomRecoveryWithWriterto 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.1golang.org/x/crypto v0.49.0golang.org/x/net v0.52.0github.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
skippedNodesstack used for wildcard fallback). - Re-ordering siblings. Each successful match calls
incrementChildPrioto bump the child's priority and reorder it toward the front, so frequently used routes are matched first. - Cap on parameters.
engine.maxParamsandengine.maxSectionstrack the largest path's parameter //-section count and drive the pre-sizedParamsslice 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:
- The chain can be paused at any point by calling
c.Next()from inside a middleware. Code beforec.Next()runs on the way in; code after runs on the way back. This is how Gin's logger captures the final status code. - The chain can be abandoned by calling
c.Abort(). Subsequent middlewares are skipped on the way back as well, because each handler'sNext()call seesc.index >= abortIndexand 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
reviveandgofumptformatting (configured in.golangci.yml). - Errors: Use
errors.Newfor static text,fmt.Errorfwith%wfor wrapping. Typed errors are tagged via*gin.Error.Typeso middleware can filter byErrorTypeBind,ErrorTypeRender,ErrorTypePrivate,ErrorTypePublic,ErrorTypeAny. - Panics for programmer error. Bad route paths, empty Basic-Auth
credentials, invalid HTTP methods, and double-running
Runare 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
*Enginevalue, not in package-level vars (with the exception ofginMode,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-Forparser doesn't accept RFC 7239Forwarded:syntax. Workaround documented in User Guide §6. - Issue #4622 (OPEN) —
SaveUploadedFilechmods existing parent directories on some filesystems. Avoid saving directly into shared directories. - Issue #4068 (OPEN) —
c.SaveUploadedFilepermissions are platform dependent; theperm ...fs.FileModevariadic 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 viac.ShouldBindUriand validator tags instead. - Issue #4332 (CLOSED) — There is no
Engine.Shutdown. Use*http.Serverdirectly (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.Handleryou 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.SetTrustedProxiesto the LB's CIDR. - Have the LB inject
X-Forwarded-ForandX-Real-IP(Gin reads both by default). - Have the LB inject
X-Forwarded-Protoif you redirect on scheme. - Disable Gin's
RedirectTrailingSlashif 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/sqlfor SQL.redis/go-redisfor Redis.mongo-driver/v2for 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.