skills/gin-go/SKILL.md
Review Go code using Gin framework for routing patterns, middleware usage, request/response handling, and error conventions.
npx skillsauth add jcleira/agent-skills gin-goInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Review Go code using the Gin web framework for best practices in routing, middleware, request/response handling, and error management.
Review router configuration for explicit control and consistent patterns.
gin.Default() without explicit middleware controlrouter.Group()// Issue: implicit middleware, no explicit control
router := gin.Default()
// Correct: explicit middleware configuration
router := gin.New()
router.Use(gin.Recovery())
router.Use(loggingMiddleware)
// Issue: ungrouped related routes
router.GET("/users/:id", h.GetUser)
router.POST("/users", h.CreateUser)
router.GET("/users/:id/orders", h.GetUserOrders)
// Correct: grouped routes
users := router.Group("/users")
{
users.GET("/:id", h.GetUser)
users.POST("", h.CreateUser)
users.GET("/:id/orders", h.GetUserOrders)
}
// Issue: wrong path naming
router.GET("/getUserById/:id", h.GetUser) // camelCase
router.GET("/user_orders", h.GetOrders) // snake_case
router.GET("/user/:id", h.GetUser) // singular
// Correct: kebab-case, plural resources
router.GET("/users/:id", h.GetUser)
router.GET("/user-orders", h.GetOrders)
Handlers should be thin and delegate business logic to services.
// Issue: business logic in handler
func (h UserHandler) Create(c *gin.Context) { // value receiver
var req CreateRequest
c.BindJSON(&req)
// Business logic - should be in service
if req.Email != "" {
// validate email format...
}
user := &User{Name: req.Name}
h.db.Create(user)
c.JSON(200, user)
}
// Correct: thin handler, pointer receiver
func (h *UserHandler) Create(c *gin.Context) {
var req CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
return
}
c.JSON(http.StatusCreated, user)
}
Middleware should follow standard patterns for cross-cutting concerns.
c.Next() is missing or misplaced in middleware// Issue: missing c.Next(), won't execute subsequent handlers
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("request received")
// Missing c.Next()!
}
}
// Correct: structured logging middleware
func LoggingMiddleware(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
logger.Info("request",
"method", c.Request.Method,
"path", path,
"status", c.Writer.Status(),
"latency", time.Since(start),
)
}
}
// Issue: auth middleware doesn't abort
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
// Missing return! Continues to handler
}
c.Next()
}
}
// Correct: abort on auth failure
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
// Validate token...
c.Set("userID", userID)
c.Next()
}
}
Use proper binding methods and struct tag validation.
BindJSON instead of ShouldBindJSONbinding:"required" tag// Issue: BindJSON aborts on error, loses control
func (h *Handler) Create(c *gin.Context) {
var req Request
c.BindJSON(&req) // Aborts with 400 on error
// Code below may not execute
}
// Correct: ShouldBindJSON for explicit error handling
func (h *Handler) Create(c *gin.Context) {
var req Request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid request body",
"details": formatValidationErrors(err),
})
return
}
}
// Issue: missing validation tags
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
// Correct: validation with struct tags
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Name string `json:"name" binding:"required,min=1,max=100"`
Age int `json:"age" binding:"omitempty,gte=0,lte=150"`
}
Maintain consistent response structure and status codes.
c.JSON called after c.Abort* methods// Issue: inconsistent response structures
c.JSON(200, user) // Direct object
c.JSON(200, gin.H{"user": user}) // Wrapped
c.JSON(200, gin.H{"data": user, "ok": true})// Different wrapper
// Correct: consistent response structure
type Response struct {
Data interface{} `json:"data,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
}
c.JSON(http.StatusOK, Response{Data: user})
// Issue: c.JSON after abort
func (h *Handler) Get(c *gin.Context) {
if !authorized {
c.AbortWithStatus(http.StatusUnauthorized)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) // Wrong!
return
}
}
// Correct: use AbortWithStatusJSON
func (h *Handler) Get(c *gin.Context) {
if !authorized {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
}
| Operation | Success | Client Error | Not Found | |-----------|---------|--------------|-----------| | GET | 200 OK | 400 Bad Request | 404 Not Found | | POST (create) | 201 Created | 400/422 | - | | PUT/PATCH | 200 OK | 400/422 | 404 Not Found | | DELETE | 204 No Content | 400 | 404 Not Found |
Handle Gin context correctly to avoid memory leaks and race conditions.
*gin.Context is stored in structs or passed to goroutinesc is passed to service layer instead of c.Request.Context()// Issue: storing gin.Context
type Handler struct {
ctx *gin.Context // Never do this!
}
// Issue: passing gin.Context to service
func (h *Handler) Get(c *gin.Context) {
h.service.Process(c) // Wrong! Pass standard context
}
// Correct: pass request context
func (h *Handler) Get(c *gin.Context) {
result, err := h.service.Process(c.Request.Context())
}
// Issue: unsafe context value retrieval
func (h *Handler) Get(c *gin.Context) {
userID := c.Get("userID").(string) // Panics if not set or wrong type
}
// Correct: safe retrieval with MustGet or type check
func (h *Handler) Get(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
uid, ok := userID.(string)
if !ok {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
}
// Issue: gin.Context in goroutine
func (h *Handler) Process(c *gin.Context) {
go func() {
// c may be reused by this point!
data := c.Query("data")
}()
}
// Correct: copy needed values before goroutine
func (h *Handler) Process(c *gin.Context) {
data := c.Query("data")
ctx := c.Request.Context()
go func() {
// Use copied values
processAsync(ctx, data)
}()
}
Centralize error handling and avoid leaking internal details.
// Issue: exposing internal errors
func (h *Handler) Get(c *gin.Context) {
user, err := h.service.GetUser(c.Request.Context(), id)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()}) // Leaks internal details!
return
}
}
// Correct: generic message, log internal error
func (h *Handler) Get(c *gin.Context) {
user, err := h.service.GetUser(c.Request.Context(), id)
if err != nil {
h.logger.ErrorContext(c.Request.Context(), "failed to get user",
"error", err,
"user_id", id,
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve user"})
return
}
}
// Correct: centralized error middleware with custom errors
type AppError struct {
Code string
Message string
Status int
Err error
}
func ErrorMiddleware(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
if appErr, ok := err.(*AppError); ok {
logger.ErrorContext(c.Request.Context(), appErr.Message,
"error", appErr.Err,
"code", appErr.Code,
)
c.JSON(appErr.Status, gin.H{
"error": gin.H{
"code": appErr.Code,
"message": appErr.Message,
},
})
return
}
// Unknown error
logger.ErrorContext(c.Request.Context(), "unhandled error", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
}
}
}
When reviewing Gin code, report findings as:
**Gin Framework Review**
Issues found:
- `handlers/user.go:15` - Using `gin.Default()`, prefer `gin.New()` with explicit middleware
- `handlers/user.go:42` - `BindJSON` should be `ShouldBindJSON` for explicit error handling
- `handlers/user.go:58` - `*gin.Context` passed to service, use `c.Request.Context()`
- `middleware/auth.go:23` - Missing `return` after `c.AbortWithStatusJSON`
Recommendations:
- Add centralized error handling middleware
- Group related routes under `/api/v1`
- Add `binding:"required"` tags to request struct fields
No issues:
- Recovery middleware configured
- Consistent JSON response structure
- Proper use of HTTP status codes
This skill auto-invokes when reviewing Go files that:
github.com/gin-gonic/gin*gin.Context parameter)gin.HandlerFunc return type)gin.New(), gin.Default(), router.Group())development
Go style and idioms from official Go Code Review Comments wiki.
development
DDD architecture review for Go projects enforcing layer boundaries, pointer semantics, and domain-driven design patterns.
development
Repository pattern review for Go codebases using Jet ORM, domain models, and adapter patterns.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.