# Gin User Guide

Gin is a fast, minimalist HTTP web framework for Go. It exposes a small,
expressive API on top of Go's `net/http` package: a tree-based router, a
chainable middleware system, a request-bound `Context` with helpers for
parameters, binding, and response rendering. The framework targets Go 1.25
or newer and is published as `github.com/gin-gonic/gin` at version `v1.12.0`.

This guide shows you how to install Gin, build your first route, and use the
features you'll reach for daily: parameter binding, JSON validation, file
upload, static file serving, custom middleware, and template rendering. For
deep architectural detail, configuration reference, and security hardening,
see the companion **Gin Administrator & Developer Guide**.

---

## 1. Quick Start

Get a Gin service running in three commands. Create a directory, initialize a
module, fetch Gin, and write a single Go file.

```bash
mkdir hello-gin && cd hello-gin
go mod init example.com/hello-gin
go get -u github.com/gin-gonic/gin
```

```text
go: added github.com/gin-gonic/gin v1.12.0
```

Create `main.go`:

```go
package main

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

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
```

Run it:

```bash
go run main.go
```

```text
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080
```

Test it from another terminal:

```bash
curl -s http://localhost:8080/ping
```

```text
{"message":"pong"}
```

That's a working Gin server: the `Default()` engine pre-attaches the `Logger`
and `Recovery` middleware, the `GET` method registers a handler on the
internal radix tree, and `Run()` blocks on `http.ListenAndServe`.

> [!TIP]
> If you don't need the default `Logger()` and `Recovery()` middleware, swap
> `gin.Default()` for `gin.New()` to start with a bare engine and attach only
> the middleware you want via `r.Use(...)`.

### Key features at a glance

- **Radix-tree routing** — parameter (`:id`) and catch-all (`*filepath`) wildcards with deterministic match order.
- **Middleware chain** — global, group-scoped, or per-route handlers via `Use()` and `Next()`/`Abort()` flow control.
- **Built-in middleware** — `gin.Logger()`, `gin.Recovery()`, `gin.BasicAuth()`, `gin.BasicAuthForProxy()`.
- **Request binding** — JSON, XML, YAML, TOML, MsgPack, Protobuf, BSON, form, query, header, URI, plain.
- **Validation** — struct-tag validation via `go-playground/validator/v10` with the `binding:` tag.
- **Response rendering** — `JSON`, `IndentedJSON`, `SecureJSON`, `JSONP`, `AsciiJSON`, `PureJSON`, `XML`, `YAML`, `TOML`, `Protobuf`, `MsgPack`, `BSON`, `PDF`, plain text, HTML templates, SSE, file streaming.
- **Server protocols** — HTTP, HTTPS (`RunTLS`), Unix socket (`RunUnix`), file descriptor (`RunFd`), HTTP/2 cleartext (`UseH2C`), QUIC/HTTP/3 (`RunQUIC`).
- **Three runtime modes** — `debug`, `release`, `test` via `gin.SetMode(...)` or the `GIN_MODE` environment variable.

### What to try next

- Section 2 (Installation) covers verification, vendoring, build tags, and tool installs.
- Section 4 (Core Usage) walks through the router, context, and rendering.
- Section 5 (Common Tasks) provides 12 ready-to-paste recipes for everyday work.

---

## 2. Installation

Gin is a Go library. It ships only as source code; there is no binary download.
The recommended path is `go get`, which writes the dependency into your
module's `go.mod` and `go.sum`.

### 2.1 Requirements

| Requirement | Value | Notes |
|---|---|---|
| Go version | **1.25 or newer** | The framework prints a `[WARNING]` if your runtime reports a lower version. |
| Module mode | Required | Gin assumes Go modules; `GOPATH`-mode installs are unsupported. |
| OS | Any Go-supported target | Linux, macOS, Windows, BSD all tested via CI. |
| Memory | Trivial | Each `*Context` is pooled via `sync.Pool` to avoid per-request allocations. |
| Network | Outbound to `proxy.golang.org` | Required during `go get`; not needed at runtime. |

> [!IMPORTANT]
> Gin v1.12.0 declares `go 1.25.0` in `go.mod`. If your toolchain is older
> than 1.25 the build will still proceed because Go's compatibility rules
> permit it, but you will see a `[WARNING] Now Gin requires Go 1.25+`
> debug message at startup. Upgrade your toolchain when you can.

### 2.2 Install via `go get` (recommended)

Inside an existing module, fetch the latest minor release:

```bash
go get -u github.com/gin-gonic/gin
```

```text
go: upgraded github.com/gin-gonic/gin v1.11.0 => v1.12.0
```

For a fresh project:

```bash
mkdir myservice && cd myservice
go mod init github.com/me/myservice
go get -u github.com/gin-gonic/gin
```

### 2.3 Pinning a specific version

Pin to a tagged release if you need deterministic builds:

```bash
go get github.com/gin-gonic/gin@v1.12.0
```

The current source-of-truth version is exposed at runtime as
`gin.Version` (the constant defined in `version.go`).

### 2.4 Build tag: omit MsgPack

The `binding` package supports MessagePack only when the `nomsgpack` build
tag is **not** set. To strip MsgPack support and shrink dependency closure:

```bash
go build -tags nomsgpack ./...
```

This selects `binding/binding_nomsgpack.go` instead of `binding/binding.go`,
removing `binding.MsgPack`, `MIMEMSGPACK`, and `MIMEMSGPACK2`. Your code must
not reference those symbols when the tag is set.

> [!WARNING]
> Mixing tagged and untagged compilation across packages of the same binary
> will cause linker errors. Apply `-tags nomsgpack` consistently to every
> `go build`, `go test`, and `go install` in the project.

### 2.5 Vendoring

For air-gapped or audit-controlled environments, vendor the module:

```bash
go mod vendor
```

```text
go: vendor: copying github.com/gin-gonic/gin
go: vendor: copying github.com/go-playground/validator/v10
...
```

Subsequent builds use `vendor/` and bypass network calls.

### 2.6 Verify the install

Create a one-line probe:

```bash
cat > main.go <<'EOF'
package main
import "github.com/gin-gonic/gin"
func main() { println(gin.Version) }
EOF
go run main.go
```

```text
v1.12.0
```

If the print matches the version you pinned, the dependency is wired correctly.

> [!TIP]
> The package import path `github.com/gin-gonic/gin` brings in the main
> `gin` symbols. The sub-packages `gin/binding`, `gin/render`, and
> `gin/ginS` are imported separately when you need their types directly.

### 2.7 Optional: the `ginS` singleton package

`ginS` (in `ginS/gins.go`) is a thin wrapper that exposes a process-wide
`*gin.Engine` initialized via `sync.OnceValue(gin.Default)`. It mirrors the
package-level routing API:

```go
import "github.com/gin-gonic/gin/ginS"

func main() {
    ginS.GET("/ping", pingHandler)
    ginS.Run()
}
```

It is intended for tiny scripts and demos. Production services should keep
their own `*gin.Engine` reference for testability and to avoid hidden global
state.

### 2.8 Install troubleshooting

#### Error: `module declares its path as: github.com/gin-gonic/gin but was required as: ...`

**Cause:** Your `go.mod` contains a `replace` directive pointing the import
path to a local fork that doesn't match the upstream module path.

**Solution:** Remove or correct the `replace` directive:

```bash
go mod edit -dropreplace github.com/gin-gonic/gin
go mod tidy
```

**Prevention:** Only use `replace` for local development; revert before commit.

#### Error: `package github.com/gin-gonic/gin: cannot find module`

**Cause:** You're outside a Go module or `GO111MODULE=off`.

**Solution:** Initialize a module and re-fetch:

```bash
go mod init mymodule
go get -u github.com/gin-gonic/gin
```

**Prevention:** Always start projects with `go mod init`.

#### Error: `unknown revision v1.12.0`

**Cause:** Your module proxy cache hasn't seen the tag yet, or the version
doesn't exist.

**Solution:** Refresh the proxy:

```bash
GOPROXY=direct go get github.com/gin-gonic/gin@v1.12.0
```

**Prevention:** Verify the tag exists in the upstream repository before pinning.

---

## 3. Configuration

Gin's runtime behavior is controlled via:

1. The **mode** (`debug`, `release`, `test`) — set at startup.
2. The **engine fields** — exposed flags on the `*gin.Engine` value.
3. **Environment variables** — `GIN_MODE` and `PORT`.
4. **Functional options** — `gin.OptionFunc` values passed into `gin.New(opts...)` or `gin.Default(opts...)`.

The full configuration reference (every field, every default, every
implication) lives in the **Admin Guide, Section 3**. This section covers
the settings users typically touch.

### 3.1 Mode

The mode controls debug logging, the default validator's behavior in test
contexts, and the route-registration print line.

```go
gin.SetMode(gin.ReleaseMode)
```

The three constants are defined in `mode.go`:

| Constant | Value | When to use |
|---|---|---|
| `gin.DebugMode` | `"debug"` | Local development. Default if `GIN_MODE` is unset and `flag.Lookup("test.v")` is nil. Prints `[GIN-debug]` lines for route registration, warnings, and errors. |
| `gin.ReleaseMode` | `"release"` | Production. Suppresses all `[GIN-debug]` output. |
| `gin.TestMode` | `"test"` | When called from `go test`. The mode auto-switches to `test` if Go's test flag is detected. |

Or set the environment variable before starting the binary:

```bash
export GIN_MODE=release
./myservice
```

The constant `gin.EnvGinMode` is `"GIN_MODE"`, the literal name read by the
`init()` function in `mode.go`.

> [!IMPORTANT]
> Always run production traffic in `gin.ReleaseMode`. Debug mode prints
> route-registration lines containing handler names, which leak internal
> structure to anyone who reads the logs. It also makes `IsDebugging()`
> true, which causes `Recovery()` to dump full request stack traces on
> panic.

### 3.2 PORT environment variable

If you call `r.Run()` with no argument, Gin reads `PORT` from the environment
and falls back to `:8080`.

```bash
export PORT=3000
go run main.go
```

```text
[GIN-debug] Environment variable PORT="3000"
[GIN-debug] Listening and serving HTTP on :3000
```

The lookup is in `utils.go:resolveAddress`. Pass an explicit address to
`Run` to override: `r.Run(":9000")`.

### 3.3 Common engine settings

These are direct fields on `*gin.Engine`. Set them before calling `Run()`:

| Field | Default | Purpose |
|---|---|---|
| `RedirectTrailingSlash` | `true` | If `/foo/` is requested but only `/foo` is registered, redirect with 301 (GET) or 307. |
| `RedirectFixedPath` | `false` | Try a case-insensitive lookup of the cleaned path before returning 404. |
| `HandleMethodNotAllowed` | `false` | Return `405 Method Not Allowed` (with `Allow` header) when a path matches but the method does not. |
| `ForwardedByClientIP` | `true` | Use `RemoteIPHeaders` to compute `c.ClientIP()` when the request comes from a trusted proxy. |
| `RemoteIPHeaders` | `["X-Forwarded-For", "X-Real-IP"]` | Headers to inspect for the real client IP. |
| `TrustedPlatform` | `""` | Set to `gin.PlatformGoogleAppEngine`, `gin.PlatformCloudflare`, or `gin.PlatformFlyIO` to trust the platform-specific IP header. |
| `MaxMultipartMemory` | `32 << 20` (32 MB) | Buffer cap for `r.ParseMultipartForm`; remainder spills to disk. |
| `UseRawPath` | `false` | Match against `URL.RawPath` instead of `URL.Path` (preserves percent encoding). |
| `UseEscapedPath` | `false` | Match against `URL.EscapedPath()`; overrides `UseRawPath` when true. |
| `UnescapePathValues` | `true` | Unescape extracted parameter values. |
| `RemoveExtraSlash` | `false` | Run `path.Clean` on the request path before matching. |
| `UseH2C` | `false` | Wrap the engine with `h2c.NewHandler` for HTTP/2 over plaintext. |
| `ContextWithFallback` | `false` | Make `c.Deadline()`, `c.Done()`, `c.Err()`, and `c.Value()` fall through to `c.Request.Context()`. |

Example:

```go
r := gin.Default()
r.RedirectFixedPath = true
r.HandleMethodNotAllowed = true
r.MaxMultipartMemory = 8 << 20 // 8 MB cap
r.SetTrustedProxies([]string{"10.0.0.0/8"})
r.Run(":8080")
```

### 3.4 Trusted proxies

When `ForwardedByClientIP` is true, Gin walks `RemoteIPHeaders` (e.g.
`X-Forwarded-For`) **only if the connecting peer is in the trusted-proxies
list**. By default that list is `["0.0.0.0/0", "::/0"]` — meaning every IP
is trusted.

Restrict it as soon as you know your network topology:

```go
err := r.SetTrustedProxies([]string{"10.0.0.0/8", "192.168.0.0/16"})
if err != nil {
    log.Fatal(err)
}
```

To disable proxy-aware client IP entirely (e.g., when Gin is the edge):

```go
r.SetTrustedProxies(nil) // c.ClientIP() returns r.RemoteAddr directly
```

> [!WARNING]
> Trusting all proxies is the default for backwards compatibility, but it
> lets any caller forge their `X-Forwarded-For`. If you log or rate-limit
> on `c.ClientIP()`, restrict the trusted-proxies CIDR list to your real
> reverse-proxy network.

The Admin Guide's **Security** section covers this in depth.

### 3.5 JSON decoder tweaks

Two package-level helpers tune the JSON binding behavior; both call into
`binding`-package globals.

```go
gin.EnableJsonDecoderUseNumber()              // calls Decoder.UseNumber()
gin.EnableJsonDecoderDisallowUnknownFields()  // rejects unknown JSON fields
```

Call these before any binding occurs (typically in `init()` or before
`router.Run`).

### 3.6 Disabling the default validator

To skip Go-tag validation entirely:

```go
gin.DisableBindValidation()
```

This sets `binding.Validator = nil`. Subsequent `c.ShouldBind*` calls
will not run struct-tag validation; you become responsible for input
checking.

---

## 4. Core Usage

This section walks through the four building blocks of every Gin
application: the engine and route groups, the request `Context`, parameter
access, and response rendering. Together they cover the vast majority of
code you will write against Gin.

### 4.1 The engine and route groups

`gin.Engine` is the framework's central type — it holds the radix-tree
router, the global middleware stack, and the configuration flags listed in
Section 3. There are two constructors:

```go
r := gin.New()      // bare engine, no middleware
r := gin.Default()  // engine + Logger() + Recovery()
```

Both accept variadic `gin.OptionFunc` arguments that mutate the engine
before it is returned. Nine HTTP method shortcuts live on the engine and on
every `*RouterGroup`:

| Method | Purpose | HTTP method registered |
|---|---|---|
| `GET(path, handlers...)` | Read | `GET` |
| `POST(path, handlers...)` | Create | `POST` |
| `PUT(path, handlers...)` | Replace | `PUT` |
| `PATCH(path, handlers...)` | Partial update | `PATCH` |
| `DELETE(path, handlers...)` | Remove | `DELETE` |
| `HEAD(path, handlers...)` | Headers only | `HEAD` |
| `OPTIONS(path, handlers...)` | CORS preflight, etc. | `OPTIONS` |
| `Any(path, handlers...)` | All nine standard methods | `GET POST PUT PATCH HEAD OPTIONS DELETE CONNECT TRACE` |
| `Match(methods, path, h...)` | Custom subset | Each method in the slice |
| `Handle(method, path, h...)` | Single arbitrary method | The given method (must be all uppercase ASCII letters) |

> [!NOTE]
> `Handle` panics if the method name fails the regex `^[A-Z]+$`. The
> validation lives in `routergroup.go` and is shared by `Any` and `Match`.

`Group` returns a `*RouterGroup` that prefixes paths and inherits the
parent's middleware:

```go
r := gin.Default()
api := r.Group("/api/v1", AuthMiddleware())
api.GET("/users/:id", getUser)
api.POST("/users", createUser)

admin := api.Group("/admin", RequireRole("admin"))
admin.DELETE("/users/:id", deleteUser)
```

The resulting routes are `/api/v1/users/:id` (auth), `/api/v1/users` (auth),
and `/api/v1/admin/users/:id` (auth + role check). `BasePath()` returns the
group's accumulated prefix.

### 4.2 Path parameters

Two wildcard styles are supported:

| Wildcard | Matches | Example |
|---|---|---|
| `:name` | Single path segment | `/user/:id` matches `/user/42` |
| `*name` | Trailing path (greedy) | `/static/*filepath` matches `/static/css/app.css` |

The leading `/` is mandatory; an `assert1` panic fires if it is missing
(`gin.go:addRoute`).

Read parameters from the context:

```go
r.GET("/user/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.String(http.StatusOK, "User %s", id)
})
```

`c.Param(key)` returns an empty string when the key is absent. Internally,
parameters are stored on `c.Params` (a `[]Param` slice). You can iterate or
look up via `c.Params.Get(name)` and `c.Params.ByName(name)`.

### 4.3 Query and form access

`*gin.Context` exposes consistent helpers for query strings and form
bodies.

| Method | Source | Returns |
|---|---|---|
| `c.Query(key)` | URL query | `string` (empty if missing) |
| `c.DefaultQuery(key, def)` | URL query | `string` (`def` if missing) |
| `c.GetQuery(key)` | URL query | `(string, bool)` |
| `c.QueryArray(key)` | URL query | `[]string` |
| `c.GetQueryArray(key)` | URL query | `([]string, bool)` |
| `c.QueryMap(key)` | URL query | `map[string]string` |
| `c.GetQueryMap(key)` | URL query | `(map[string]string, bool)` |
| `c.PostForm(key)` | Form body | `string` |
| `c.DefaultPostForm(key, def)` | Form body | `string` |
| `c.GetPostForm(key)` | Form body | `(string, bool)` |
| `c.PostFormArray(key)` | Form body | `[]string` |
| `c.GetPostFormArray(key)` | Form body | `([]string, bool)` |
| `c.PostFormMap(key)` | Form body | `map[string]string` |
| `c.GetPostFormMap(key)` | Form body | `(map[string]string, bool)` |
| `c.FormFile(name)` | Multipart | `(*multipart.FileHeader, error)` |
| `c.MultipartForm()` | Multipart | `(*multipart.Form, error)` |

Both query and form caches are populated lazily on first access and reused
within the request lifecycle.

### 4.4 Binding request bodies into structs

The `Bind*` and `ShouldBind*` family parses request input directly into a
typed struct, optionally validating against `binding:` tags.

| Helper | Source | Sets 400 + AbortWithError on failure? |
|---|---|---|
| `c.Bind(obj)` | Auto-detected by method + Content-Type | Yes |
| `c.BindJSON(obj)` | JSON body | Yes |
| `c.BindXML(obj)` | XML body | Yes |
| `c.BindQuery(obj)` | URL query | Yes |
| `c.BindYAML(obj)` | YAML body | Yes |
| `c.BindTOML(obj)` | TOML body | Yes |
| `c.BindPlain(obj)` | Plain text body | Yes |
| `c.BindHeader(obj)` | Request headers | Yes |
| `c.BindUri(obj)` | URL path params | Yes |
| `c.MustBindWith(obj, b)` | Caller-supplied `binding.Binding` | Yes |
| `c.ShouldBind(obj)` | Auto-detected | No (returns error) |
| `c.ShouldBindJSON(obj)` | JSON body | No |
| `c.ShouldBindXML(obj)` | XML body | No |
| `c.ShouldBindQuery(obj)` | URL query | No |
| `c.ShouldBindYAML(obj)` | YAML body | No |
| `c.ShouldBindTOML(obj)` | TOML body | No |
| `c.ShouldBindPlain(obj)` | Plain text body | No |
| `c.ShouldBindHeader(obj)` | Headers | No |
| `c.ShouldBindUri(obj)` | URL path params | No |
| `c.ShouldBindWith(obj, b)` | Caller-supplied binding | No |
| `c.ShouldBindBodyWith(obj, bb)` | Buffered body (re-readable) | No |
| `c.ShouldBindBodyWithJSON(obj)` | Buffered JSON | No |
| `c.ShouldBindBodyWithXML(obj)` | Buffered XML | No |
| `c.ShouldBindBodyWithYAML(obj)` | Buffered YAML | No |
| `c.ShouldBindBodyWithTOML(obj)` | Buffered TOML | No |
| `c.ShouldBindBodyWithPlain(obj)` | Buffered plain text | No |

The `ShouldBindBodyWith*` variants buffer the raw request body so multiple
binders can run against it (useful when you want to try JSON first and fall
back to YAML, for example).

The `binding` package exposes the underlying engines as package variables:
`binding.JSON`, `binding.XML`, `binding.Form`, `binding.Query`,
`binding.FormPost`, `binding.FormMultipart`, `binding.ProtoBuf`,
`binding.MsgPack` (default build), `binding.YAML`, `binding.Uri`,
`binding.Header`, `binding.Plain`, `binding.TOML`, and `binding.BSON`.
Pass any of these to `ShouldBindWith` for explicit binder selection.

> [!TIP]
> Prefer the `ShouldBind*` variants in idiomatic Go code. They return the
> error so you can craft a tailored response. The `Bind*` variants are
> ergonomic but write a 400 status before you can intervene.

### 4.5 Validation tags

The default validator (`binding/default_validator.go`) wraps
`go-playground/validator/v10` and reads the `binding:` struct tag.

```go
type CreateUser struct {
    Email    string `json:"email"    binding:"required,email"`
    Username string `json:"username" binding:"required,min=3,max=32"`
    Age      int    `json:"age"      binding:"gte=13,lte=120"`
    Website  string `json:"website"  binding:"omitempty,url"`
}
```

The tag name is hardcoded as `binding` in `defaultValidator.lazyinit`. The
validator instance is shared across requests (`sync.Once` lazy init), so
register custom rules once at startup:

```go
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
    v.RegisterValidation("strongpassword", strongPassword)
}
```

### 4.6 Rendering responses

Each renderer is a method on `*gin.Context`. They write the appropriate
`Content-Type` header and serialize the value.

| Method | Content-Type | Notes |
|---|---|---|
| `c.JSON(code, obj)` | `application/json` | Standard JSON |
| `c.IndentedJSON(code, obj)` | `application/json` | Pretty-printed; ~10x slower than `JSON` |
| `c.SecureJSON(code, obj)` | `application/json` | Prefixes the configurable `secureJSONPrefix` (default `while(1);`) when the top-level value is an array |
| `c.JSONP(code, obj)` | `application/javascript` | Wraps in the `?callback=` query string |
| `c.AsciiJSON(code, obj)` | `application/json` | Escapes non-ASCII bytes |
| `c.PureJSON(code, obj)` | `application/json` | Disables HTML escaping in the encoder |
| `c.XML(code, obj)` | `application/xml` | Uses `encoding/xml` |
| `c.YAML(code, obj)` | `application/yaml` | Uses `goccy/go-yaml` |
| `c.TOML(code, obj)` | `application/toml` | Uses `pelletier/go-toml/v2` |
| `c.ProtoBuf(code, obj)` | `application/x-protobuf` | `obj` must implement `proto.Message` |
| `c.MsgPack(code, obj)` | `application/x-msgpack` | Available unless `nomsgpack` build tag |
| `c.BSON(code, obj)` | `application/bson` | Uses `mongo-driver/v2/bson` |
| `c.PDF(code, data)` | `application/pdf` | `data` is the raw PDF byte slice |
| `c.HTML(code, name, obj)` | `text/html; charset=utf-8` | Renders a registered template |
| `c.String(code, fmt, vals...)` | `text/plain; charset=utf-8` | `fmt.Sprintf`-style |
| `c.Data(code, ct, data)` | Caller-provided | Raw bytes |
| `c.DataFromReader(...)` | Caller-provided | Streams an `io.Reader` |
| `c.File(filepath)` | Detected by Go stdlib | `http.ServeFile` |
| `c.FileFromFS(filepath, fs)` | Detected | Serve from a custom `http.FileSystem` |
| `c.FileAttachment(path, name)` | Detected | Sets `Content-Disposition: attachment; filename=...` |
| `c.Redirect(code, location)` | Standard | 30x redirect |
| `c.SSEvent(name, msg)` | `text/event-stream` | Writes a single SSE chunk |
| `c.Stream(step)` | Caller-provided | Loops `step(w)` until it returns false or the client disconnects |
| `c.Render(code, render.Render)` | Renderer-defined | Low-level escape hatch |

The `gin.H` shortcut (`type H map[string]any`) is the idiomatic way to
build ad-hoc JSON payloads.

```go
c.JSON(http.StatusOK, gin.H{
    "status": "ok",
    "items":  results,
})
```

> [!NOTE]
> `gin.H` implements `xml.Marshaler` (in `utils.go`), so it serializes to
> a sensible XML element tree as well — convenient if your API supports
> content negotiation.

### 4.7 Content negotiation

Use `c.Negotiate(code, gin.Negotiate{...})` to render the format the client
prefers based on the `Accept` header. The full set of formats Gin ships
with:

```go
c.Negotiate(http.StatusOK, gin.Negotiate{
    Offered: []string{
        gin.MIMEJSON, gin.MIMEXML, gin.MIMEYAML, gin.MIMETOML,
        gin.MIMEHTML, gin.MIMEPROTOBUF, gin.MIMEBSON,
    },
    Data:     payload,
    HTMLName: "details", // only used for MIMEHTML
})
```

Gin parses `Accept`, selects the best match against `Offered`, and
dispatches to the matching renderer. If no overlap is found, the response
is `406 Not Acceptable` and the error `"the accepted formats are not
offered by the server"` is attached to the context.

### 4.8 Middleware basics

A middleware is just a `gin.HandlerFunc` that may inspect/modify the
request, optionally call `c.Next()` to invoke the rest of the chain, then
run code on the way back.

```go
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        id := uuid.NewString()
        c.Set("requestID", id)
        c.Header("X-Request-ID", id)
        c.Next() // run the next handler
        // post-processing, e.g. metrics, runs here
    }
}

r.Use(RequestID())
```

Three flow-control methods govern the chain:

| Method | Effect |
|---|---|
| `c.Next()` | Executes pending handlers; usable inside any handler. |
| `c.Abort()` | Marks the chain done; the next `c.Next()` is a no-op. |
| `c.IsAborted()` | True if the chain was aborted. |
| `c.AbortWithStatus(code)` | Writes status + aborts. |
| `c.AbortWithStatusJSON(code, obj)` | Writes JSON + aborts. |
| `c.AbortWithStatusPureJSON(code, obj)` | Writes pure (unescaped) JSON + aborts. |
| `c.AbortWithError(code, err)` | Writes status + appends error + aborts. |

Built-in middleware:

| Function | Purpose |
|---|---|
| `gin.Logger()` | Default formatter writes `[GIN] timestamp \| status \| latency \| client IP \| method path` to `gin.DefaultWriter` (stdout). |
| `gin.LoggerWithFormatter(f)` | Same as `Logger()` with a custom `LogFormatter`. |
| `gin.LoggerWithWriter(w, skipPaths...)` | Custom writer + skip list. |
| `gin.LoggerWithConfig(LoggerConfig{...})` | Full control: `Formatter`, `Output`, `SkipPaths`, `SkipQueryString`, `Skip Skipper`. |
| `gin.Recovery()` | Recovers from `recover()`-able panics, writes `500`, dumps stack to `gin.DefaultErrorWriter`. |
| `gin.CustomRecovery(handle)` | Same as `Recovery()` with a caller-defined handler. |
| `gin.RecoveryWithWriter(out, recovery...)` | Same with custom writer. |
| `gin.CustomRecoveryWithWriter(out, handle)` | Both knobs. |
| `gin.BasicAuth(accounts)` | Adds HTTP Basic Auth using the `Accounts` map. |
| `gin.BasicAuthForRealm(accounts, realm)` | Same with custom realm. |
| `gin.BasicAuthForProxy(accounts, realm)` | Same but uses `Proxy-Authorization` and returns 407. |
| `gin.ErrorLogger()` / `gin.ErrorLoggerT(typ)` | After-the-fact error renderer for any type. |
| `gin.Bind(struct)` | Helper that auto-binds before the handler runs. |
| `gin.WrapF(f)` / `gin.WrapH(h)` | Wrap a stdlib `http.HandlerFunc` or `http.Handler` as a Gin handler. |

> [!TIP]
> Gin authenticates a Basic-Auth request in constant time using
> `crypto/subtle.ConstantTimeCompare`. The comparison is on the full
> `"Basic base64(user:pass)"` value so single-character credentials can
> still benefit from timing-safe matching.

### 4.9 Server lifecycle

`r.Run()` is the most common starter, but six other listeners are
available:

| Method | Listens on |
|---|---|
| `r.Run(addr...)` | TCP. With no arg, reads `PORT` (defaults to `:8080`). |
| `r.RunTLS(addr, certFile, keyFile)` | HTTPS via `http.ListenAndServeTLS`. |
| `r.RunUnix(file)` | Unix socket. |
| `r.RunFd(fd)` | Existing file descriptor (e.g., systemd socket activation). |
| `r.RunListener(net.Listener)` | Any pre-opened listener. |
| `r.RunQUIC(addr, certFile, keyFile)` | QUIC/HTTP/3 via `quic-go`. |
| `r.Handler()` | Returns the `http.Handler` for embedding (handles `UseH2C` if set). |

All of them block until the underlying server returns. To run alongside
other goroutines, launch in `go func() { ... }()`.

> [!WARNING]
> Calling `Run` (or any `Run*`) more than once on the same engine will
> fail because the underlying TCP port is bound twice. Returns an error
> instead of panicking; check it.

---

## 5. Common Tasks

Twelve recipes covering the workloads you'll hit in the first week. Each
follows the same structure: **Goal**, **Prerequisites**, **Steps**,
**Verify**, **If it fails**.

### 5.1 Bind and validate a JSON request body

**Goal:** Accept a JSON POST, validate required fields, return a typed
error.

**Prerequisites:** Gin installed, struct definition with `json:` and
`binding:` tags.

**Steps:**

```go
type Signup struct {
    Email    string `json:"email"    binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
    Age      int    `json:"age"      binding:"gte=13"`
}

r.POST("/signup", func(c *gin.Context) {
    var in Signup
    if err := c.ShouldBindJSON(&in); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    // ... persist user ...
    c.JSON(http.StatusCreated, gin.H{"id": 42})
})
```

**Verify:**

```bash
curl -s -X POST http://localhost:8080/signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email removed]","password":"longenough","age":21}'
```

```text
{"id":42}
```

**If it fails:** `{"error":"Key: 'Signup.Email' Error:Field validation for
'Email' failed on the 'email' tag"}` — the validator rejected one or more
fields. Fix the input or relax the tag.

### 5.2 Single file upload

**Goal:** Accept a `multipart/form-data` upload and persist it.

**Prerequisites:** `MaxMultipartMemory` configured; destination directory
writable.

**Steps:**

```go
r.MaxMultipartMemory = 8 << 20 // 8 MB

r.POST("/upload", func(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.String(http.StatusBadRequest, "no file: %s", err)
        return
    }
    if err := c.SaveUploadedFile(file, "./uploads/"+file.Filename); err != nil {
        c.String(http.StatusInternalServerError, "save failed: %s", err)
        return
    }
    c.String(http.StatusOK, "saved %s (%d bytes)", file.Filename, file.Size)
})
```

**Verify:**

```bash
curl -s -F "file=@./README.md" http://localhost:8080/upload
```

```text
saved README.md (12345 bytes)
```

**If it fails:** Check `c.Request.Body` size limits and that the destination
directory exists with the correct permissions. `SaveUploadedFile` accepts
an optional `fs.FileMode` to control the saved file mode.

> [!WARNING]
> See open issue #4622 — `SaveUploadedFile` may chmod parent directories
> on some filesystems. If you save into `/tmp` directly, prefer a
> dedicated subdirectory you create yourself with `os.MkdirAll(..., 0755)`.

### 5.3 Multiple file upload

**Goal:** Accept many files in one request.

**Prerequisites:** As above.

**Steps:**

```go
r.POST("/uploads", func(c *gin.Context) {
    form, err := c.MultipartForm()
    if err != nil {
        c.String(http.StatusBadRequest, "bad form: %s", err)
        return
    }
    files := form.File["files"]
    saved := 0
    for _, f := range files {
        if err := c.SaveUploadedFile(f, "./uploads/"+f.Filename); err == nil {
            saved++
        }
    }
    c.JSON(http.StatusOK, gin.H{"saved": saved, "total": len(files)})
})
```

**Verify:**

```bash
curl -s -F "files=@a.txt" -F "files=@b.txt" http://localhost:8080/uploads
```

```text
{"saved":2,"total":2}
```

**If it fails:** Confirm the form field name matches (`files`, not `file`).

### 5.4 Serve a static directory

**Goal:** Expose `/static/...` from a local folder.

**Prerequisites:** Public assets stored in `./public/`.

**Steps:**

```go
r.Static("/static", "./public")           // serves ./public/* at /static/*
r.StaticFile("/favicon.ico", "./favicon.ico")
```

**Verify:**

```bash
curl -sI http://localhost:8080/static/css/app.css
```

```text
HTTP/1.1 200 OK
Content-Type: text/css; charset=utf-8
```

**If it fails:** Confirm the path on disk; `panic: URL parameters can not
be used when serving a static folder` indicates you used `:` or `*` in the
prefix (not allowed; the wildcard is added internally).

> [!NOTE]
> The internal handler uses `http.FileServer` and replies with
> `http.NotFound` when the file is missing, **bypassing the engine's
> NoRoute handler**. To customize 404 behavior on static, wrap with
> `StaticFS` and your own `http.FileSystem`.

### 5.5 Custom request-ID middleware

**Goal:** Stamp every request with an ID and echo it in responses.

**Steps:**

```go
import "crypto/rand"
import "encoding/hex"

func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        b := make([]byte, 8)
        rand.Read(b)
        id := hex.EncodeToString(b)
        c.Set("requestID", id)
        c.Header("X-Request-ID", id)
        c.Next()
    }
}

r.Use(RequestID())
r.GET("/whoami", func(c *gin.Context) {
    c.JSON(200, gin.H{"id": c.MustGet("requestID")})
})
```

**Verify:**

```bash
curl -si http://localhost:8080/whoami | head
```

```text
HTTP/1.1 200 OK
X-Request-Id: a1b2c3d4e5f60718
Content-Type: application/json; charset=utf-8

{"id":"a1b2c3d4e5f60718"}
```

**If it fails:** Confirm `r.Use(RequestID())` is called before route
registration; middleware ordering matters.

### 5.6 Render HTML templates

**Goal:** Return a server-rendered HTML page.

**Prerequisites:** Templates in `./views/*.tmpl`.

**Steps:**

```go
r.LoadHTMLGlob("views/*.tmpl")
r.GET("/hello/:name", func(c *gin.Context) {
    c.HTML(http.StatusOK, "hello.tmpl", gin.H{
        "name": c.Param("name"),
    })
})
```

`./views/hello.tmpl`:

```html
<html><body>Hello, {{.name}}!</body></html>
```

**Verify:**

```bash
curl -s http://localhost:8080/hello/Ada
```

```text
<html><body>Hello, Ada!</body></html>
```

**If it fails:** `panic: html/template: "hello.tmpl" is undefined` —
confirm the glob pattern matches and `LoadHTMLGlob` runs **before** the
route is hit. To swap delimiters: `r.Delims("[[", "]]")` before
`LoadHTMLGlob`.

> [!TIP]
> `r.LoadHTMLFiles(file1, file2, ...)` registers an explicit list. In
> debug mode, both helpers swap to `HTMLDebug` which re-parses templates
> on every request — handy for live editing, slow for production.

### 5.7 Group routes with shared middleware

**Goal:** Put auth on `/api/*` while leaving `/health` open.

**Steps:**

```go
r := gin.Default()
r.GET("/health", func(c *gin.Context) { c.String(200, "ok") })

api := r.Group("/api/v1", AuthMiddleware())
{
    api.GET("/users/:id", getUser)
    api.POST("/users", createUser)
}

admin := api.Group("/admin", RequireRole("admin"))
{
    admin.DELETE("/users/:id", deleteUser)
}
```

**Verify:** `/health` returns 200 unauthenticated; `/api/v1/users/1` returns
401 without a token; `/api/v1/admin/users/1` returns 403 without admin role.

**If it fails:** Each `Group(...)` call returns a *new* `*RouterGroup`;
make sure you register routes on the returned value, not the parent.

### 5.8 JSONP for legacy cross-origin clients

**Goal:** Reply to `?callback=foo` with `foo({...})`.

**Steps:**

```go
r.GET("/jsonp", func(c *gin.Context) {
    c.JSONP(http.StatusOK, gin.H{"hello": "world"})
})
```

**Verify:**

```bash
curl -s "http://localhost:8080/jsonp?callback=cb"
```

```text
cb({"hello":"world"});
```

**If it fails:** If `?callback=` is missing, the response falls back to
plain JSON (no wrapping), which is the documented behavior.

### 5.9 Server-Sent Events (SSE)

**Goal:** Push a stream of updates to a long-lived client.

**Steps:**

```go
r.GET("/events", func(c *gin.Context) {
    c.Stream(func(w io.Writer) bool {
        c.SSEvent("tick", time.Now().Format(time.RFC3339))
        time.Sleep(time.Second)
        select {
        case <-c.Request.Context().Done():
            return false // client gone
        default:
            return true  // keep streaming
        }
    })
})
```

**Verify:**

```bash
curl -N http://localhost:8080/events
```

```text
event: tick
data: 2026-04-28T14:50:00Z

event: tick
data: 2026-04-28T14:50:01Z
```

**If it fails:** Some intermediate proxies buffer responses; set
`X-Accel-Buffering: no` on the response or test through a TCP-direct path.

### 5.10 Graceful shutdown

**Goal:** Drain in-flight requests before exiting.

**Prerequisites:** Use the `*http.Server` directly via `r.Handler()`.

**Steps:**

```go
srv := &http.Server{Addr: ":8080", Handler: r.Handler()}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("listen: %s", err)
    }
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    log.Fatalf("shutdown: %s", err)
}
log.Println("server exited")
```

**Verify:** Send SIGTERM mid-request — the response completes; new requests
get connection refused only after the drain timeout.

**If it fails:** `r.Run()` does not expose `Shutdown`. Issue #4332 (CLOSED)
documented this gap; the workaround above is the standard pattern.

> [!IMPORTANT]
> Gin does not ship a built-in graceful shutdown helper. Use the
> `http.Server.Shutdown` API as shown — it is the canonical Go solution.

### 5.11 Custom error rendering with the error stack

**Goal:** Collect handler errors and render them as a JSON payload.

**Steps:**

```go
r.Use(func(c *gin.Context) {
    c.Next()
    if len(c.Errors) > 0 {
        c.JSON(http.StatusInternalServerError, gin.H{
            "errors": c.Errors.Errors(), // []string
        })
    }
})

r.GET("/calc", func(c *gin.Context) {
    if _, err := strconv.Atoi(c.Query("n")); err != nil {
        c.Error(err) // pushes to c.Errors with ErrorTypePrivate
        c.AbortWithStatus(http.StatusBadRequest)
        return
    }
    c.String(http.StatusOK, "ok")
})
```

**Verify:**

```bash
curl -si http://localhost:8080/calc?n=abc
```

```text
HTTP/1.1 400 Bad Request

{"errors":["strconv.Atoi: parsing \"abc\": invalid syntax"]}
```

**If it fails:** Note `c.Error(err)` panics if `err` is nil; guard before
calling.

### 5.12 Versioned API with URL prefix

**Goal:** Run `/api/v1` and `/api/v2` side by side.

**Steps:**

```go
r := gin.Default()
v1 := r.Group("/api/v1")
v1.GET("/users/:id", v1GetUser)

v2 := r.Group("/api/v2")
v2.GET("/users/:id", v2GetUser)
```

**Verify:**

```bash
curl -s http://localhost:8080/api/v1/users/1
curl -s http://localhost:8080/api/v2/users/1
```

Both return their version-specific payload.

**If it fails:** If you accidentally register `/api/v1/users/:id` twice
with different handlers, you'll see a panic at startup — Gin enforces
unique paths per HTTP method during route registration in `tree.go`.

---

## 6. Troubleshooting

### Diagnostic decision tree

1. **Is the request hitting Gin at all?** Check the `[GIN]` log line. If
   the line is absent, traffic is not reaching your binary — check the
   proxy, firewall, or `Run` address.
2. **Got a `404 page not found` body?** The path is unmatched. Compare the
   incoming `URL.Path` against your registered routes; remember
   `RedirectTrailingSlash` and `RedirectFixedPath` semantics.
3. **Got `405 method not allowed`?** Set `r.HandleMethodNotAllowed = true`;
   the path matches but no handler exists for the request method.
4. **Got a `500` with a stack trace?** A panic was caught by `Recovery()`.
   The stack is printed to stderr; the response body is empty.
5. **Got `400` with a binding error?** A `Bind*` helper failed validation
   and short-circuited. Switch to `ShouldBind*` for full control.

### Error: `cannot read nil body`

**Cause:** A binder was called on a request whose `Body` was already
consumed (e.g., another middleware called `io.ReadAll(r.Body)` first).

**Solution:** Buffer the body before any consumer reads it:

```go
import "bytes"
import "io"

raw, _ := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewReader(raw))
// ... later ...
c.ShouldBindJSON(&obj)
```

Or use `c.ShouldBindBodyWith*`, which buffers automatically and stores the
result under `gin.BodyBytesKey` for later re-binding.

**Prevention:** Avoid reading `c.Request.Body` directly in middleware.

### Error: `the accepted formats are not offered by the server`

**Cause:** `c.Negotiate(...)` was called with an `Offered` list that does
not overlap with the request's `Accept` header.

**Solution:** Add the requested MIME to `Offered`, or fall back to a
default format with `c.NegotiateFormat(offered...)`.

**Prevention:** Wildcard-match by including `*/*` handlers, or trim the
list to the formats your service actually supports.

### Error: `gin: response body already written`

**Cause:** A handler called `c.Writer.Write(...)` (or a renderer) twice on
the same context, or after `c.AbortWithStatusJSON` already wrote the body.

**Solution:** Pick one writer per request. Check `c.Writer.Written()` if
unsure:

```go
if !c.Writer.Written() {
    c.JSON(http.StatusOK, gin.H{"data": data})
}
```

**Prevention:** Audit middleware that writes responses; design so only the
final handler renders.

### Error: `unknown type` (binding/form_mapping.go:23)

**Cause:** The form binding encountered a field whose type cannot be
mapped from a `[]string` (e.g., a struct without a
`UnmarshalParam`/`UnmarshalText` method, or an exotic kind).

**Solution:** Implement `encoding.TextUnmarshaler` on the field type, or
add a `form:"-"` tag to skip:

```go
type CreatedAt time.Time
func (c *CreatedAt) UnmarshalText(b []byte) error { /* ... */ }
```

**Prevention:** Keep form-bound structs to plain primitives, slices of
primitives, or types that implement `TextUnmarshaler`.

### Error: `invalid request` (binding/json.go)

**Cause:** The JSON binder received a non-pointer or nil pointer as the
target value. The full message is `"invalid request"`.

**Solution:** Pass `&obj`, not `obj`, into `ShouldBindJSON`.

**Prevention:** Treat the binder's first argument as `*T` always.

### Error: `Empty list of authorized credentials`

**Cause:** `BasicAuth(nil)` or `BasicAuth(map[string]string{})` was
called.

**Solution:** Provide at least one user/password pair:

```go
r.Use(gin.BasicAuth(gin.Accounts{"admin": "secret"}))
```

**Prevention:** Validate your config loader before passing credentials to
the middleware.

### Error: `URL parameters can not be used when serving a static folder`

**Cause:** The relative path passed to `Static`, `StaticFS`, `StaticFile`,
or `StaticFileFS` contains `:` or `*`. These are reserved for routing
wildcards.

**Solution:** Use a static prefix:

```go
r.Static("/static", "./public") // not "/static/:type"
```

**Prevention:** Keep static-asset prefixes literal.

### Error: `path must begin with '/'`

**Cause:** A route was registered without a leading slash:
`r.GET("ping", ...)` instead of `r.GET("/ping", ...)`. The `assert1` panic
fires inside `engine.addRoute`.

**Solution:** Add the slash. Always.

**Prevention:** Lint or unit-test route registration to catch typos at
startup.

### Error: `http method X is not valid`

**Cause:** `r.Handle("delete", ...)` (lowercase) or `r.Handle("DEL", ...)`
(non-letter chars). Gin requires `^[A-Z]+$`.

**Solution:** Use the all-caps method name: `http.MethodDelete`
(`"DELETE"`).

**Prevention:** Use the named constants from `net/http` rather than string
literals.

### Error: `gin mode unknown: <value>`

**Cause:** `gin.SetMode("foo")` was called with a value other than
`debug`, `release`, or `test`. The list is enforced in `mode.go:SetMode`.

**Solution:**

```go
gin.SetMode(gin.ReleaseMode) // or DebugMode / TestMode
```

**Prevention:** Use the package constants.

### Issue: Trailing slash redirects break some clients

**Cause:** With `RedirectTrailingSlash` (default `true`), a request for
`/foo/` is 301'd to `/foo` (or vice versa). Clients that don't follow
redirects, or POSTs that drop the body on 307, can break.

**Solution:** Disable the behavior:

```go
r.RedirectTrailingSlash = false
```

**Prevention:** Decide your URL canonicalization policy at the proxy or
gateway layer, not inside the handler.

### Issue: `c.ClientIP()` always returns `127.0.0.1`

**Cause:** A reverse proxy is in front but `SetTrustedProxies` excludes
the proxy's IP, so Gin falls back to `RemoteAddr`.

**Solution:** Add the proxy CIDR:

```go
r.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12"})
```

**Prevention:** Restrict trusted proxies to your real network and verify
with a test request whose `X-Forwarded-For` you control.

### Issue: `X-Forwarded-For` reading non-standard formats

**Cause:** Issue #4572 (OPEN) — Gin parses `X-Forwarded-For` as a
comma-separated list of plain IPs. Some proxies emit
`for=192.0.2.43; proto=https` (RFC 7239 syntax) which Gin does not
currently parse.

**Solution:** If your proxy emits RFC 7239 `Forwarded:`, parse it manually
in middleware and call `c.Set("clientIP", parsedIP)` for downstream
handlers.

**Prevention:** Configure your proxy to emit a plain `X-Forwarded-For`
list.

---

## 7. FAQ

**Q: How do I disable Gin's debug logging?**

Set the mode to release: either `gin.SetMode(gin.ReleaseMode)` at startup
or `export GIN_MODE=release` in the environment.

**Q: How do I customize the access log format?**

Pass a `LoggerConfig` with a custom `Formatter`:

```go
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
    Formatter: func(p gin.LogFormatterParams) string {
        return fmt.Sprintf("%s %d %s %s\n",
            p.TimeStamp.Format(time.RFC3339), p.StatusCode, p.Method, p.Path)
    },
}))
```

The struct exposes `Output`, `SkipPaths`, `SkipQueryString`, and a `Skip`
function for finer control.

**Q: Can I run Gin on multiple ports?**

Yes — call `r.Run(":8080")` and `r.Run(":9090")` from separate goroutines.
Each call binds its own listener. They share the same router.

**Q: How do I serve files from an embedded `embed.FS`?**

Wrap it with `http.FS()` and pass to `LoadHTMLFS` or `StaticFS`:

```go
//go:embed views
var viewsFS embed.FS

r.LoadHTMLFS(http.FS(viewsFS), "views/*.tmpl")
r.StaticFS("/assets", http.FS(viewsFS))
```

**Q: How do I get the matched route pattern for the current request?**

`c.FullPath()` returns the registered pattern (e.g., `/user/:id`), not the
incoming URL. Use it for metrics/labels to avoid high cardinality from
varying path parameters.

**Q: How do I share data between middleware and a handler?**

Use the typed `c.Set`/`c.Get` API:

```go
c.Set("userID", uid)
// later:
uid := c.MustGet("userID").(string)
```

For typed shorthand: `c.GetString`, `c.GetInt`, `c.GetBool`, `c.GetTime`,
`c.GetDuration`, `c.GetFloat64`, `c.GetStringSlice`, `c.GetStringMap`,
`c.GetStringMapString`, `c.GetStringMapStringSlice`, plus integer slice
variants for every signed/unsigned width.

**Q: Does Gin support HTTP/2?**

Yes. `RunTLS` uses `golang.org/x/net/http2` automatically. For HTTP/2 over
plaintext (h2c), set `r.UseH2C = true` and use `r.Run(...)`. The handler
chain wraps with `h2c.NewHandler` when you call `r.Handler()`.

**Q: Does Gin support HTTP/3?**

Yes. `r.RunQUIC(addr, certFile, keyFile)` listens via `quic-go`. The
listener serves HTTP/3 only — pair it with `RunTLS` or a separate HTTP/2
binary if you need both.

**Q: What's the difference between `Bind()` and `ShouldBind()`?**

`Bind()` writes a 400 response and aborts the chain on error;
`ShouldBind()` returns the error to the caller. Use `ShouldBind*` for
production handlers where you want to format the error response yourself.

**Q: Does Gin ship CSRF protection?**

No. CSRF is a cross-cutting concern handled by middleware libraries
(commonly the `gin-contrib` ecosystem) or by enforcing same-site cookie
attributes. Use `c.SetSameSite(http.SameSiteStrictMode)` on session
cookies and pair with a CSRF token middleware.

**Q: How big can multipart uploads be?**

`MaxMultipartMemory` (default 32 MB) controls the in-memory buffer; data
beyond that spills to disk via the standard library. There is no separate
hard cap on body size — wrap with `http.MaxBytesReader` if you need one:

```go
r.Use(func(c *gin.Context) {
    c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 64<<20)
})
```

**Q: How do I run tests against my Gin handlers?**

Use `httptest.NewRecorder()` and `r.ServeHTTP(w, req)`:

```go
gin.SetMode(gin.TestMode)
r := setupRouter()
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
r.ServeHTTP(w, req)
require.Equal(t, 200, w.Code)
```

The `TestMode` switch silences debug warnings and avoids touching real
ports.

**Q: How do I exclude a middleware from specific routes?**

Issue #3047 (CLOSED) discussed this; the canonical answer is **don't**.
Build separate route groups instead — apply the middleware only where you
need it. If absolutely required, gate the middleware on `c.FullPath()`:

```go
func MaybeAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.FullPath() == "/health" {
            c.Next()
            return
        }
        // ... auth logic ...
    }
}
```

**Q: Where is the framework's version reported?**

`gin.Version` (constant in `version.go`). At commit `d3ffc99` it is
`"v1.12.0"`.

---

_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. Unofficial; not affiliated with or reviewed by the project._
