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()andRecovery()middleware, swapgin.Default()forgin.New()to start with a bare engine and attach only the middleware you want viar.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()andNext()/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/v10with thebinding: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,testviagin.SetMode(...)or theGIN_MODEenvironment 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.0ingo.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:
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 nomsgpackconsistently to everygo build,go test, andgo installin 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/ginbrings in the mainginsymbols. The sub-packagesgin/binding,gin/render, andgin/ginSare 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:
- The mode (
debug,release,test) — set at startup. - The engine fields — exposed flags on the
*gin.Enginevalue. - Environment variables —
GIN_MODEandPORT. - Functional options —
gin.OptionFuncvalues passed intogin.New(opts...)orgin.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 makesIsDebugging()true, which causesRecovery()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 onc.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
Handlepanics if the method name fails the regex^[A-Z]+$. The validation lives inroutergroup.goand is shared byAnyandMatch.
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. TheBind*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.Himplementsxml.Marshaler(inutils.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 anyRun*) 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 —
SaveUploadedFilemay chmod parent directories on some filesystems. If you save into/tmpdirectly, prefer a dedicated subdirectory you create yourself withos.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.FileServerand replies withhttp.NotFoundwhen the file is missing, bypassing the engine's NoRoute handler. To customize 404 behavior on static, wrap withStaticFSand your ownhttp.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 toHTMLDebugwhich 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.ShutdownAPI 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
- 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, orRunaddress. - Got a
404 page not foundbody? The path is unmatched. Compare the incomingURL.Pathagainst your registered routes; rememberRedirectTrailingSlashandRedirectFixedPathsemantics. - Got
405 method not allowed? Setr.HandleMethodNotAllowed = true; the path matches but no handler exists for the request method. - Got a
500with a stack trace? A panic was caught byRecovery(). The stack is printed to stderr; the response body is empty. - Got
400with a binding error? ABind*helper failed validation and short-circuited. Switch toShouldBind*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".