Gin Core Knowledge

SkillMedia

Gin Go web framework. Covers routing, middleware, binding, validation, and rendering. Use for fast, minimalist Go APIs.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Gin Core Knowledge skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/backend-frameworks/gin/SKILL.md and read by ahel’s review.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: gin for comprehensive documentation.

Basic Setup

package main

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

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

    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello, World!")
    })

    r.Run(":8080")
}

Routing

Basic Routes

r := gin.Default()

r.GET("/users", listUsers)
r.GET("/users/:id", getUser)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)

// Any HTTP method
r.Any("/any", handleAny)

// NoRoute handler
r.NoRoute(func(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})

Path Parameters

// Single parameter
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(http.StatusOK, gin.H{"id": id})
})

// Wildcard
r.GET("/files/*filepath", func(c *gin.Context) {
    filepath := c.Param("filepath")
    c.String(http.StatusOK, "File: %s", filepath)
})

Route Groups

api := r.Group("/api")
{
    v1 := api.Group("/v1")
    {
        v1.GET("/users", listUsersV1)
        v1.POST("/users", createUserV1)
    }

    v2 := api.Group("/v2")
    {
        v2.GET("/users", listUsersV2)
        v2.POST("/users", createUserV2)
    }
}

Request Binding

JSON Binding

type CreateUserRequest struct {
    Name  string `json:"name" binding:"required"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age" binding:"gte=0,lte=130"`
}

func createUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "name":  req.Name,
        "email": req.Email,
    })
}

Query Binding

type ListUsersQuery struct {
    Page    int    `form:"page" binding:"gte=1"`
    PerPage int    `form:"per_page" binding:"gte=1,lte=100"`
    Sort    string `form:"sort"`
}

func listUsers(c *gin.Context) {
    var query ListUsersQuery
    query.Page = 1
    query.PerPage = 20

    if err := c.ShouldBindQuery(&query); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "page":     query.Page,
        "per_page": query.PerPage,
    })
}

Form Binding

type LoginForm struct {
    Username string `form:"username" binding:"required"`
    Password string `form:"password" binding:"required"`
}

func login(c *gin.Context) {
    var form LoginForm
    if err := c.ShouldBind(&form); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"username": form.Username})
}

URI Binding

type UserURI struct {
    ID uint `uri:"id" binding:"required"`
}

func getUser(c *gin.Context) {
    var uri UserURI
    if err := c.ShouldBindUri(&uri); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"id": uri.ID})
}

Validation

Custom Validator

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

var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
    username := fl.Field().String()
    return len(username) >= 3 && len(username) <= 20
}

func main() {
    r := gin.Default()

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("username", validateUsername)
    }

    r.Run(":8080")
}

type RegisterRequest struct {
    Username string `json:"username" binding:"required,username"`
    Email    string `json:"email" binding:"required,email"`
}

Middleware

Built-in Middleware

r := gin.New()

r.Use(gin.Logger())
r.Use(gin.Recovery())

r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
    return fmt.Sprintf("%s - [%s] %s %s %d %s\n",
        param.ClientIP,
        param.TimeStamp.Format(time.RFC1123),
        param.Method,
        param.Path,
        param.StatusCode,
        param.Latency,
    )
}))

Custom Middleware

func TimingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        duration := time.Since(start)
        c.Header("X-Response-Time", duration.String())
    }
}

func main() {
    r := gin.Default()
    r.Use(TimingMiddleware())
}

Authentication Middleware

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")

        if token == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
            c.Abort()
            return
        }

        if !strings.HasPrefix(token, "Bearer ") {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid format"})
            c.Abort()
            return
        }

        tokenString := token[7:]
        user, err := validateToken(tokenString)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            c.Abort()
            return
        }

        c.Set("user", user)
        c.Next()
    }
}

protected := r.Group("/api")
protected.Use(AuthMiddleware())
{
    protected.GET("/me", getMe)
}

func getMe(c *gin.Context) {
    user, _ := c.Get("user")
    c.JSON(http.StatusOK, user)
}

CORS Middleware

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

func main() {
    r := gin.Default()

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

    r.Run(":8080")
}

Response Rendering

JSON Response

func getUser(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "id":   1,
        "name": "Alice",
    })
}

type User struct {
    ID   uint   `json:"id"`
    Name string `json:"name"`
}

func getUser(c *gin.Context) {
    user := User{ID: 1, Name: "Alice"}
    c.JSON(http.StatusOK, user)
}

Other Formats

c.XML(http.StatusOK, gin.H{"message": "hello"})
c.YAML(http.StatusOK, gin.H{"message": "hello"})
c.String(http.StatusOK, "Hello, %s!", name)
c.Redirect(http.StatusMovedPermanently, "https://example.com")
c.File("./static/file.txt")

Production Readiness

Configuration

func main() {
    gin.SetMode(gin.ReleaseMode)

    r := gin.New()
    r.Use(gin.Recovery())
    r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
        Formatter: jsonLogFormatter,
        Output:    os.Stdout,
    }))

    r.Run(":8080")
}

func jsonLogFormatter(param gin.LogFormatterParams) string {
    log := map[string]interface{}{
        "timestamp": param.TimeStamp.Format(time.RFC3339),
        "status":    param.StatusCode,
        "latency":   param.Latency.Milliseconds(),
        "method":    param.Method,
        "path":      param.Path,
    }
    b, _ := json.Marshal(log)
    return string(b) + "\n"
}

Health Checks

func healthHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"status": "healthy"})
}

func readyHandler(db *sql.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
        defer cancel()

        if err := db.PingContext(ctx); err != nil {
            c.JSON(http.StatusServiceUnavailable, gin.H{
                "status":   "not ready",
                "database": "disconnected",
            })
            return
        }

        c.JSON(http.StatusOK, gin.H{
            "status":   "ready",
            "database": "connected",
        })
    }
}

Graceful Shutdown

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }
}

Checklist

  • gin.ReleaseMode for production
  • CORS configured
  • Authentication middleware
  • Request validation
  • Structured JSON logging
  • Health/readiness endpoints
  • Graceful shutdown
  • Rate limiting

When NOT to Use This Skill

  • Echo projects - Echo has different middleware patterns
  • Fiber projects - Fiber is Express-like with different API
  • Chi projects - Chi is stdlib-compatible router
  • gRPC services - Use gRPC framework directly
  • Minimal stdlib apps - Gin adds dependency overhead

Anti-Patterns

Anti-PatternWhy It's BadSolution
Using gin.Default() without knowingIncludes logger/recovery you may not wantUse gin.New() and add middleware explicitly
Not checking binding errorsInvalid data processedAlways check ShouldBindJSON() error
Using c.String() for JSONManual serialization error-proneUse c.JSON() instead
Global variables for configHard to testPass config via context or dependency injection
Missing validation tagsNo input validationUse binding:"required,email" tags
Not setting gin.ReleaseModeDebug logs in productionSet gin.SetMode(gin.ReleaseMode)

Quick Troubleshooting

ProblemDiagnosisFix
Route not found (404)Route not registeredCheck router setup and method
Binding fails silentlyNot checking errorCheck ShouldBindJSON() return value
CORS errorsNot configuredUse gin-contrib/cors middleware
Middleware not executingWrong orderPlace middleware before routes
Panic in handlerNo recovery middlewareUse gin.Recovery()
Slow JSON serializationLarge responseUse c.Stream() or pagination

Reference Documentation

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
gin
Source
github.com/claude-dev-suite/claude-dev-suite