Skip to content

FAQ

General Questions

How do I enable live reload during development?

Use Air for automatic live reloading during development. Air watches your files and rebuilds/restarts your application when changes are detected.

Installation:

Terminal window
go install github.com/air-verse/air@latest

Setup:

Create a .air.toml configuration file in your project root:

Terminal window
air init

Then run air in your project directory instead of go run:

Terminal window
air

Air will watch your .go files and automatically rebuild/restart your Gin application on changes. See the Air documentation for configuration options.

How do I handle CORS in Gin?

Use the official gin-contrib/cors middleware:

package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Default CORS configuration
r.Use(cors.Default())
// Or customize CORS settings
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run()
}

For a complete security overview, see Security best practices.

How do I serve static files?

Use Static() or StaticFS() to serve static files:

func main() {
r := gin.Default()
// Serve files from ./assets directory at /assets/*
r.Static("/assets", "./assets")
// Serve a single file
r.StaticFile("/favicon.ico", "./resources/favicon.ico")
// Serve from embedded filesystem (Go 1.16+)
r.StaticFS("/public", http.FS(embedFS))
r.Run()
}

See Serving data from file for more details.

How do I handle file uploads?

Use FormFile() for single files or MultipartForm() for multiple files:

// Single file upload
r.POST("/upload", func(c *gin.Context) {
file, _ := c.FormFile("file")
c.SaveUploadedFile(file, "./uploads/"+file.Filename)
c.String(200, "File %s uploaded successfully", file.Filename)
})
// Multiple files upload
r.POST("/upload-multiple", func(c *gin.Context) {
form, _ := c.MultipartForm()
files := form.File["files"]
for _, file := range files {
c.SaveUploadedFile(file, "./uploads/"+file.Filename)
}
c.String(200, "%d files uploaded", len(files))
})

See the Upload file documentation for more details.

How do I implement authentication with JWT?

Use gin-contrib/jwt or implement custom middleware. Here’s a minimal example:

package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-secret-key")
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization token"})
c.Abort()
return
}
// Remove "Bearer " prefix if present
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
if claims, ok := token.Claims.(*Claims); ok {
c.Set("username", claims.Username)
c.Next()
}
}
}

For session-based authentication, see Session management.

How do I set up request logging?

Gin includes a default logger middleware via gin.Default(). For structured JSON logging in production, see Structured logging.

For basic log customization:

r := gin.New()
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{"/healthz"},
}))
r.Use(gin.Recovery())

See the Logging section for all options including custom formats, file output, and skipping query strings.

How do I handle graceful shutdown?

See Graceful restart or stop for a complete guide with code examples.

Why am I getting “404 Not Found” instead of “405 Method Not Allowed”?

By default, Gin returns 404 for routes that don’t support the requested HTTP method. Set HandleMethodNotAllowed = true to return 405 instead:

r := gin.Default()
r.HandleMethodNotAllowed = true
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run()
Terminal window
$ curl -X POST localhost:8080/ping
HTTP/1.1 405 Method Not Allowed
Allow: GET

How do I bind query parameters and POST data together?

Use ShouldBind() which automatically selects the binding based on content type:

type User struct {
Name string `form:"name" json:"name"`
Email string `form:"email" json:"email"`
Page int `form:"page"`
}
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBind(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

See the Binding section for all binding options.

How do I validate request data?

Gin uses go-playground/validator for validation. Add validation tags to your structs:

type User struct {
Name string `json:"name" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=130"`
}
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"message": "User is valid"})
})

See Binding and validation for custom validators and advanced usage.

How do I run Gin in production mode?

Set the GIN_MODE environment variable to release:

Terminal window
export GIN_MODE=release
# or
GIN_MODE=release ./your-app

Or set it programmatically:

gin.SetMode(gin.ReleaseMode)

Release mode disables debug logging and improves performance.

How do I handle database connections with Gin?

See Database integration for a complete guide covering database/sql, GORM, connection pooling, and dependency injection patterns.

How do I test Gin handlers?

Use net/http/httptest to test your routes:

func TestPingRoute(t *testing.T) {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "pong")
}

See the Testing documentation for more examples.

Performance Questions

How do I optimize Gin for high traffic?

  1. Use Release Mode: Set GIN_MODE=release
  2. Disable unnecessary middleware: Only use what you need
  3. Use gin.New() instead of gin.Default() for manual middleware control
  4. Connection pooling: Configure database connection pools (see Database integration)
  5. Caching: Implement caching for frequently accessed data
  6. Load balancing: Use reverse proxy (nginx, HAProxy)
  7. Profiling: Use Go’s pprof to identify bottlenecks
  8. Monitoring: Set up metrics and monitoring to track performance

Is Gin production-ready?

Yes. Gin is used in production by many companies and has been battle-tested at scale. See Users for examples of projects using Gin in production.

Troubleshooting

Why are my route parameters not working?

Ensure route parameters use : syntax and are properly extracted:

// Correct
r.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id")
c.String(200, "User ID: %s", id)
})
// Not: /user/{id} or /user/<id>

See Parameters in path for details.

Why is my middleware not executing?

Middleware must be registered before routes or route groups:

// Correct order
r := gin.New()
r.Use(MyMiddleware()) // Register middleware first
r.GET("/ping", handler) // Then routes
// For route groups
auth := r.Group("/admin")
auth.Use(AuthMiddleware()) // Middleware for this group
{
auth.GET("/dashboard", handler)
}

See Using middleware for details.

Why is request binding failing?

Common reasons:

  1. Missing binding tags: Add json:"field" or form:"field" tags
  2. Content-Type mismatch: Ensure client sends correct Content-Type header
  3. Validation errors: Check validation tags and requirements
  4. Unexported fields: Only exported (capitalized) struct fields are bound
type User struct {
Name string `json:"name" binding:"required"` // ✓ Correct
Email string `json:"email"` // ✓ Correct
age int `json:"age"` // ✗ Won't bind (unexported)
}

See Binding and validation for details.