Jesse A. Green / notebook

Gin User Guide

Generated April 2026 from gin-gonic/gin at d3ffc99

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

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.

mkdir hello-gin && cd hello-gin
go mod init example.com/hello-gin
go get -u github.com/gin-gonic/gin
go: added github.com/gin-gonic/gin v1.12.0

Create main.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:

go run main.go
[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:

curl -s http://localhost:8080/ping
{"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.

Inside an existing module, fetch the latest minor release:

go get -u github.com/gin-gonic/gin
go: upgraded github.com/gin-gonic/gin v1.11.0 => v1.12.0

For a fresh project:

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:

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:

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:

go mod vendor
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:

cat > main.go <<'EOF'
package main
import "github.com/gin-gonic/gin"
func main() { println(gin.Version) }
EOF
go run main.go
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:

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:

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:

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:

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.

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:

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.

export PORT=3000
go run main.go
[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:

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:

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):

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.

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:

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:

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:

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:

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.

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:

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.

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:

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.

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:

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:

curl -s -X POST http://localhost:8080/signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email removed]","password":"longenough","age":21}'
{"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:

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:

curl -s -F "file=@./README.md" http://localhost:8080/upload
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:

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:

curl -s -F "files=@a.txt" -F "files=@b.txt" http://localhost:8080/uploads
{"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:

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

Verify:

curl -sI http://localhost:8080/static/css/app.css
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:

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:

curl -si http://localhost:8080/whoami | head
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:

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><body>Hello, {{.name}}!</body></html>

Verify:

curl -s http://localhost:8080/hello/Ada
<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:

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:

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

Verify:

curl -s "http://localhost:8080/jsonp?callback=cb"
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:

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:

curl -N http://localhost:8080/events
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:

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:

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:

curl -si http://localhost:8080/calc?n=abc
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:

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

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

Verify:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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: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:

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:

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):

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():

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".