Go Gin Framework Cheatsheet

Routing, middleware, request binding, JSON responses, auth, database & deployment patterns

Framework / Backend
Contents
βš™οΈ

Setup & Hello World

// Install
// go get -u github.com/gin-gonic/gin

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()  // includes Logger + Recovery middleware

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })

    r.Run(":8080")  // listen on 0.0.0.0:8080
}

// gin.H is a shortcut for map[string]any
πŸ›€οΈ

Routing & Route Groups

// HTTP methods
r.GET("/users", getUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.PATCH("/users/:id", patchUser)
r.DELETE("/users/:id", deleteUser)

// Route groups (shared prefix + middleware)
api := r.Group("/api/v1")
{
    api.GET("/users", getUsers)
    api.POST("/users", createUser)

    // Nested group with auth middleware
    admin := api.Group("/admin")
    admin.Use(AuthMiddleware())
    {
        admin.GET("/stats", getStats)
        admin.DELETE("/users/:id", deleteUser)
    }
}

// Static files
r.Static("/assets", "./public")
r.StaticFile("/favicon.ico", "./public/favicon.ico")

// No route (404 handler)
r.NoRoute(func(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})
πŸ“Ž

Params, Query Strings & Headers

// Path parameters
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")   // string
    c.JSON(200, gin.H{"id": id})
})

// Wildcard
r.GET("/files/*filepath", func(c *gin.Context) {
    path := c.Param("filepath")  // e.g., "/docs/readme.md"
})

// Query parameters
// GET /search?q=golang&page=2&limit=10
r.GET("/search", func(c *gin.Context) {
    q := c.Query("q")                     // "golang"
    page := c.DefaultQuery("page", "1")    // "2"
    limit := c.DefaultQuery("limit", "20") // "10"
})

// Headers
token := c.GetHeader("Authorization")
c.Header("X-Request-ID", "abc123")
πŸ“¦

Request Binding (JSON, Form, URI)

type CreateUserInput struct {
    Name  string `json:"name" binding:"required,min=2,max=100"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age" binding:"required,gte=1,lte=150"`
}

r.POST("/users", func(c *gin.Context) {
    var input CreateUserInput

    // ShouldBindJSON β€” returns error, doesn't abort
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, gin.H{"user": input})
})

// Bind URI params
type UserURI struct {
    ID int `uri:"id" binding:"required"`
}
r.GET("/users/:id", func(c *gin.Context) {
    var uri UserURI
    c.ShouldBindUri(&uri)
})

// Bind query params
type SearchQuery struct {
    Q     string `form:"q"`
    Page  int    `form:"page,default=1"`
    Limit int    `form:"limit,default=20"`
}
r.GET("/search", func(c *gin.Context) {
    var q SearchQuery
    c.ShouldBindQuery(&q)
})
πŸ“€

Responses

// JSON
c.JSON(http.StatusOK, gin.H{"message": "success"})
c.JSON(200, user)  // struct serialization

// IndentedJSON (pretty-printed)
c.IndentedJSON(200, data)

// String
c.String(200, "Hello %s", name)

// Status only
c.Status(http.StatusNoContent)  // 204

// Redirect
c.Redirect(http.StatusMovedPermanently, "https://example.com")

// File download
c.File("./files/report.pdf")
c.FileAttachment("./files/report.pdf", "report.pdf")

// Streaming
c.Stream(func(w io.Writer) bool {
    fmt.Fprintf(w, "data: %s\n\n", msg)
    return keepStreaming
})
πŸ”Œ

Middleware

// Custom middleware
func RequestLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()

        c.Next()  // process request

        latency := time.Since(start)
        status := c.Writer.Status()
        log.Printf("[%d] %s %s (%v)",
            status, c.Request.Method, c.Request.URL.Path, latency)
    }
}

// Apply globally
r.Use(RequestLogger())

// CORS middleware
import "github.com/gin-contrib/cors"

r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"http://localhost:3000"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Authorization", "Content-Type"},
    AllowCredentials: true,
    MaxAge:           12 * time.Hour,
}))

// Rate limiter
import "github.com/gin-contrib/ratelimit"

// Recovery (built-in) β€” recovers from panics
r.Use(gin.Recovery())
πŸ”

Authentication (JWT)

import "github.com/golang-jwt/jwt/v5"

var jwtSecret = []byte(os.Getenv("JWT_SECRET"))

// Generate token
func GenerateToken(userID uint) (string, error) {
    claims := jwt.MapClaims{
        "user_id": userID,
        "exp":     time.Now().Add(24 * time.Hour).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

// Auth middleware
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        header := c.GetHeader("Authorization")
        if header == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
            return
        }

        tokenStr := strings.TrimPrefix(header, "Bearer ")
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
            return jwtSecret, nil
        })

        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
            return
        }

        claims := token.Claims.(jwt.MapClaims)
        c.Set("user_id", claims["user_id"])
        c.Next()
    }
}

// Use in handler
userID := c.MustGet("user_id").(float64)
βœ…

Validation Tags

// Common validation tags (uses go-playground/validator)
type Input struct {
    Name     string `binding:"required"`
    Email    string `binding:"required,email"`
    Age      int    `binding:"gte=0,lte=150"`
    Password string `binding:"required,min=8,max=64"`
    URL      string `binding:"url"`
    Role     string `binding:"oneof=admin user moderator"`
    Tags     []string `binding:"dive,min=1,max=50"`
}

// Custom validator
import "github.com/go-playground/validator/v10"

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    v.RegisterValidation("nowhitespace", func(fl validator.FieldLevel) bool {
        return !strings.Contains(fl.Field().String(), " ")
    })
}
πŸ—„οΈ

Database (GORM Integration)

import (
    "gorm.io/gorm"
    "gorm.io/driver/postgres"
)

type User struct {
    gorm.Model              // ID, CreatedAt, UpdatedAt, DeletedAt
    Name  string `json:"name"`
    Email string `json:"email" gorm:"uniqueIndex"`
    Posts []Post `json:"posts"`
}

type Post struct {
    gorm.Model
    Title    string `json:"title"`
    Content  string `json:"content"`
    UserID   uint   `json:"user_id"`
}

// Connect
dsn := "host=localhost user=dev password=pass dbname=app port=5432"
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&User{}, &Post{})

// CRUD in handlers
r.GET("/users", func(c *gin.Context) {
    var users []User
    db.Preload("Posts").Find(&users)
    c.JSON(200, users)
})

r.POST("/users", func(c *gin.Context) {
    var user User
    c.ShouldBindJSON(&user)
    result := db.Create(&user)
    if result.Error != nil {
        c.JSON(400, gin.H{"error": result.Error.Error()})
        return
    }
    c.JSON(201, user)
})
❌

Error Handling

// Standardized error response
type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        if len(c.Errors) > 0 {
            err := c.Errors.Last()
            c.JSON(c.Writer.Status(), APIError{
                Code:    c.Writer.Status(),
                Message: err.Error(),
            })
        }
    }
}

// Usage in handler
r.GET("/users/:id", func(c *gin.Context) {
    user, err := findUser(c.Param("id"))
    if err != nil {
        c.AbortWithStatusJSON(404, gin.H{"error": "user not found"})
        return
    }
    c.JSON(200, user)
})
πŸ§ͺ

Testing

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/stretchr/testify/assert"
)

func setupRouter() *gin.Engine {
    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")
}

// Test with JSON body
func TestCreateUser(t *testing.T) {
    r := setupRouter()
    body := strings.NewReader(`{"name":"Alice","email":"a@b.com"}`)
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/users", body)
    req.Header.Set("Content-Type", "application/json")
    r.ServeHTTP(w, req)
    assert.Equal(t, 201, w.Code)
}
🐳

Deployment

# Multi-stage Dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]

# Set Gin to release mode in production
# export GIN_MODE=release