Neural Architecture Hub
Go

Gin

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

Gin Developer Guide


Installation & Setup

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

Minimal Server

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
}

Engine Initialization

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

HTTP Methods (Basic Routing)

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

Route Parameters

// 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})
})

Query Parameters

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

Reading Request Body

// 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})
})

Binding & Validation

// 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"`
}

Sending Responses

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

Status Code Constants (net/http)

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

Headers & Cookies

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

Middleware

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

Route Groups

// 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)
    }
}

Context: Passing Data Between Handlers

// 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")

File Uploads

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

HTML Templates

// 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 Error Handling & 404/405

// 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)
}))

Graceful Shutdown

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
}

TLS / HTTPS

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

Streaming Responses

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

Testing with httptest

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)

// 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()
})

Logger Configuration

// 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"},
}))

Common Patterns

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

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

On this page