Skip to content

Gin framework commands, routing, middleware, binding, and response handling reference.


Terminal window
go mod init my-app # Initialize a new Go module
go get github.com/gin-gonic/gin # Install Gin framework
go run main.go # Run the application
go build -o app . # Build the binary

package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // Router with Logger + Recovery middleware
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run(":8080") // Listen on 0.0.0.0:8080
}

r := gin.Default() // Logger + Recovery middleware included
r := gin.New() // Bare engine, no middleware
gin.SetMode(gin.DebugMode) // Default mode (verbose logging)
gin.SetMode(gin.ReleaseMode) // Production mode (minimal logging)
gin.SetMode(gin.TestMode) // Testing mode

r.GET("/path", handler) # Handle GET requests
r.POST("/path", handler) # Handle POST requests
r.PUT("/path", handler) # Handle PUT requests
r.PATCH("/path", handler) # Handle PATCH requests
r.DELETE("/path", handler) # Handle DELETE requests
r.HEAD("/path", handler) # Handle HEAD requests
r.OPTIONS("/path", handler) # Handle OPTIONS requests
r.Any("/path", handler) # Handle all HTTP methods

// URL parameters
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // Exact segment: /users/42
c.JSON(200, gin.H{"id": id})
})
// Wildcard parameters
r.GET("/files/*filepath", func(c *gin.Context) {
path := c.Param("filepath") // Everything after /files/: /a/b/c.txt
c.JSON(200, gin.H{"path": path})
})

r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // "" if missing
page := c.DefaultQuery("page", "1") // "1" if missing
exists := c.QueryMap("filters") // map[string]string
tags, ok := c.GetQueryArray("tag") // []string, bool
c.JSON(200, gin.H{"q": q, "page": page, "tags": tags, "ok": ok})
})

// Bind JSON body
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
r.POST("/login", func(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"user": input.Username})
})
// Bind form data
r.POST("/form", func(c *gin.Context) {
name := c.PostForm("name") // "" if missing
color := c.DefaultPostForm("color", "red")
c.JSON(200, gin.H{"name": name, "color": color})
})

// ShouldBind* — returns error, does NOT abort automatically
c.ShouldBindJSON(&obj) // Bind JSON body
c.ShouldBindQuery(&obj) // Bind query parameters
c.ShouldBindUri(&obj) // Bind URI parameters
c.ShouldBindHeader(&obj) // Bind headers
c.ShouldBind(&obj) // Auto-detect (JSON, form, XML...)
// MustBind* — calls c.AbortWithError on failure (use ShouldBind instead)
c.BindJSON(&obj)
c.BindQuery(&obj)
// Struct tags used by binding
type User struct {
Name string `json:"name" form:"name" uri:"name" binding:"required"`
Email string `json:"email" form:"email" binding:"required,email"`
Age int `json:"age" form:"age" binding:"gte=0,lte=130"`
}

c.JSON(200, gin.H{"message": "ok"}) // JSON response
c.JSON(http.StatusOK, gin.H{"key": "val"}) // Using net/http constants
c.IndentedJSON(200, obj) // Pretty-printed JSON
c.XML(200, obj) // XML response
c.YAML(200, obj) // YAML response
c.String(200, "Hello %s", "World") // Plain text
c.HTML(200, "index.tmpl", gin.H{"title": "Hi"}) // HTML template
c.Redirect(302, "/new-path") // HTTP redirect
c.Data(200, "application/pdf", pdfBytes) // Raw bytes
c.File("./public/file.pdf") // Serve a file
c.FileAttachment("./report.pdf", "report.pdf") // Force download
c.AbortWithStatus(403) // Abort with status only
c.AbortWithStatusJSON(400, gin.H{"err": "bad"}) // Abort with JSON body

import "net/http"
http.StatusOK // 200
http.StatusCreated // 201
http.StatusNoContent // 204
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusForbidden // 403
http.StatusNotFound // 404
http.StatusInternalServerError // 500

// Reading headers
token := c.GetHeader("Authorization") // Single header value
lang := c.Request.Header.Get("Accept-Language")
// Setting response headers
c.Header("X-Custom-Header", "value")
c.Header("Cache-Control", "no-cache")
// Cookies
c.SetCookie("session", "abc123", 3600, "/", "localhost", false, true)
// name value maxAge path domain secure httpOnly
val, err := c.Cookie("session") // Read a cookie

// Global middleware
r := gin.New()
r.Use(gin.Logger()) // Request logging
r.Use(gin.Recovery()) // Panic recovery → 500
r.Use(myCustomMiddleware()) // Custom global middleware
// Writing a middleware
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return // Must return after Abort
}
c.Set("userID", 42) // Pass data to next handler
c.Next() // Call the next handler
// Code here runs AFTER the handler (post-processing)
}
}
// Applying middleware to a single route
r.GET("/secure", AuthRequired(), handler)

// Basic group
api := r.Group("/api")
{
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.GET("/users/:id", getUser)
}
// Group with middleware
v1 := r.Group("/v1", AuthRequired())
{
v1.GET("/profile", getProfile)
v1.PUT("/profile", updateProfile)
}
// Nested groups
admin := r.Group("/admin", AuthRequired(), AdminOnly())
{
users := admin.Group("/users")
{
users.GET("", listAdminUsers)
users.DELETE("/:id", deleteUser)
}
}

// Set a value in middleware
c.Set("userID", 42)
c.Set("user", &User{Name: "Alice"})
// Get a value in the next handler
userID, exists := c.Get("userID") // returns (any, bool)
if !exists { /* handle missing */ }
// Type-safe getters (panic if type mismatch)
id := c.GetInt("userID")
name := c.GetString("username")
flag := c.GetBool("isAdmin")
f := c.GetFloat64("score")

// Single file
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file") // field name
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst) // Save to disk
c.JSON(200, gin.H{"filename": file.Filename})
})
// Multiple files
r.POST("/upload-multi", func(c *gin.Context) {
form, _ := c.MultipartForm()
files := form.File["files"] // field name
for _, file := range files {
c.SaveUploadedFile(file, "./uploads/"+file.Filename)
}
c.JSON(200, gin.H{"count": len(files)})
})
r.MaxMultipartMemory = 8 << 20 // 8 MiB (default 32 MiB)

// Load templates
r.LoadHTMLGlob("templates/*") // All files in templates/
r.LoadHTMLFiles("tmpl/a.html", "tmpl/b.html") // Specific files
// Render a template
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Home",
"user": "Alice",
})
})
// Serve static files
r.Static("/assets", "./static") // Directory as static
r.StaticFile("/favicon.ico", "./static/favicon.ico") // Single file
r.StaticFS("/files", http.Dir("./uploads")) // Custom fs

// Custom 404 handler
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{"error": "route not found"})
})
// Custom 405 handler
r.NoMethod(func(c *gin.Context) {
c.JSON(405, gin.H{"error": "method not allowed"})
})
// Handle panic in middleware with Recovery
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered any) {
if err, ok := recovered.(string); ok {
c.JSON(500, gin.H{"error": err})
}
c.AbortWithStatus(500)
}))

import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
r := gin.Default()
srv := &http.Server{Addr: ":8080", Handler: r}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // Block until signal received
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx) // Graceful shutdown with timeout
}

r.RunTLS(":443", "cert.pem", "key.pem") // HTTPS with cert files
r.RunAutoTLS(":443") // Auto TLS via Let's Encrypt (autocert)
// Or using http.Server directly
srv := &http.Server{
Addr: ":443",
Handler: r,
TLSConfig: tlsCfg,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.ListenAndServeTLS("cert.pem", "key.pem")

r.GET("/stream", func(c *gin.Context) {
c.Stream(func(w io.Writer) bool {
// Return false to stop streaming
msg := <-messageChan
if msg == "" {
return false
}
c.SSEvent("message", msg) // Server-Sent Event
return true
})
})
// Server-Sent Events (SSE) manually
r.GET("/sse", func(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
for i := 0; i < 5; i++ {
fmt.Fprintf(c.Writer, "data: event %d\n\n", i)
c.Writer.Flush()
time.Sleep(1 * time.Second)
}
})

import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func setupRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
return r
}
func TestPing(t *testing.T) {
r := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "pong")
}

Useful Built-in Middleware (from gin-contrib)

Section titled “Useful Built-in Middleware (from gin-contrib)”
// CORS — github.com/gin-contrib/cors
import "github.com/gin-contrib/cors"
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// Rate limiting — github.com/ulule/limiter
// Sessions — github.com/gin-contrib/sessions
// JWT — github.com/golang-jwt/jwt
// Request ID
r.Use(func(c *gin.Context) {
c.Set("requestID", uuid.New().String())
c.Header("X-Request-ID", c.GetString("requestID"))
c.Next()
})

// Custom logger format
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[%s] %s %s %d %s\n",
param.TimeStamp.Format(time.RFC3339),
param.Method,
param.Path,
param.StatusCode,
param.Latency,
)
}))
// Skip logging certain paths
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{"/health", "/metrics"},
}))

// Health check endpoint
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Versioned API
v1 := r.Group("/api/v1")
v2 := r.Group("/api/v2")
// Dependency injection via closure
func NewUserHandler(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// use db here
c.JSON(200, gin.H{"status": "ok"})
}
}
r.GET("/users", NewUserHandler(db))
// Abort chain early
func StopChain() gin.HandlerFunc {
return func(c *gin.Context) {
c.Abort() // Stops middleware chain, does NOT write response
// c.AbortWithStatus(403) // Stops AND writes status
}
}

Quick Reference — c *gin.Context Methods

Section titled “Quick Reference — c *gin.Context Methods”
// Request info
c.Request.Method // "GET", "POST", etc.
c.FullPath() // Registered route: "/users/:id"
c.ClientIP() // Best-guess client IP
c.ContentType() // "application/json", etc.
c.IsWebsocket() // true if Upgrade: websocket
// Response control
c.Status(202) // Set status code only
c.Writer.Written() // true if response headers sent
c.Writer.Status() // Current status code
c.Writer.Size() // Bytes written so far
// Copy context for async use
cp := c.Copy() // Safe to use in goroutines
go func(ctx *gin.Context) {
// use cp, never c in goroutines
}(cp)