Gin Developer Guide
Section titled “Gin Developer Guide”Installation & Setup
Section titled “Installation & Setup”go mod init my-app # Initialize a new Go modulego get github.com/gin-gonic/gin # Install Gin frameworkgo run main.go # Run the applicationgo build -o app . # Build the binaryMinimal Server
Section titled “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
Section titled “Engine Initialization”r := gin.Default() // Logger + Recovery middleware includedr := gin.New() // Bare engine, no middlewaregin.SetMode(gin.DebugMode) // Default mode (verbose logging)gin.SetMode(gin.ReleaseMode) // Production mode (minimal logging)gin.SetMode(gin.TestMode) // Testing modeHTTP Methods (Basic Routing)
Section titled “HTTP Methods (Basic Routing)”r.GET("/path", handler) # Handle GET requestsr.POST("/path", handler) # Handle POST requestsr.PUT("/path", handler) # Handle PUT requestsr.PATCH("/path", handler) # Handle PATCH requestsr.DELETE("/path", handler) # Handle DELETE requestsr.HEAD("/path", handler) # Handle HEAD requestsr.OPTIONS("/path", handler) # Handle OPTIONS requestsr.Any("/path", handler) # Handle all HTTP methodsRoute Parameters
Section titled “Route Parameters”// URL parametersr.GET("/users/:id", func(c *gin.Context) { id := c.Param("id") // Exact segment: /users/42 c.JSON(200, gin.H{"id": id})})
// Wildcard parametersr.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
Section titled “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
Section titled “Reading Request Body”// Bind JSON bodytype 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 datar.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
Section titled “Binding & Validation”// ShouldBind* — returns error, does NOT abort automaticallyc.ShouldBindJSON(&obj) // Bind JSON bodyc.ShouldBindQuery(&obj) // Bind query parametersc.ShouldBindUri(&obj) // Bind URI parametersc.ShouldBindHeader(&obj) // Bind headersc.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 bindingtype 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
Section titled “Sending Responses”c.JSON(200, gin.H{"message": "ok"}) // JSON responsec.JSON(http.StatusOK, gin.H{"key": "val"}) // Using net/http constantsc.IndentedJSON(200, obj) // Pretty-printed JSONc.XML(200, obj) // XML responsec.YAML(200, obj) // YAML responsec.String(200, "Hello %s", "World") // Plain textc.HTML(200, "index.tmpl", gin.H{"title": "Hi"}) // HTML templatec.Redirect(302, "/new-path") // HTTP redirectc.Data(200, "application/pdf", pdfBytes) // Raw bytesc.File("./public/file.pdf") // Serve a filec.FileAttachment("./report.pdf", "report.pdf") // Force downloadc.AbortWithStatus(403) // Abort with status onlyc.AbortWithStatusJSON(400, gin.H{"err": "bad"}) // Abort with JSON bodyStatus Code Constants (net/http)
Section titled “Status Code Constants (net/http)”import "net/http"
http.StatusOK // 200http.StatusCreated // 201http.StatusNoContent // 204http.StatusBadRequest // 400http.StatusUnauthorized // 401http.StatusForbidden // 403http.StatusNotFound // 404http.StatusInternalServerError // 500Headers & Cookies
Section titled “Headers & Cookies”// Reading headerstoken := c.GetHeader("Authorization") // Single header valuelang := c.Request.Header.Get("Accept-Language")
// Setting response headersc.Header("X-Custom-Header", "value")c.Header("Cache-Control", "no-cache")
// Cookiesc.SetCookie("session", "abc123", 3600, "/", "localhost", false, true)// name value maxAge path domain secure httpOnly
val, err := c.Cookie("session") // Read a cookieMiddleware
Section titled “Middleware”// Global middlewarer := gin.New()r.Use(gin.Logger()) // Request loggingr.Use(gin.Recovery()) // Panic recovery → 500r.Use(myCustomMiddleware()) // Custom global middleware
// Writing a middlewarefunc 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 router.GET("/secure", AuthRequired(), handler)Route Groups
Section titled “Route Groups”// Basic groupapi := r.Group("/api"){ api.GET("/users", listUsers) api.POST("/users", createUser) api.GET("/users/:id", getUser)}
// Group with middlewarev1 := r.Group("/v1", AuthRequired()){ v1.GET("/profile", getProfile) v1.PUT("/profile", updateProfile)}
// Nested groupsadmin := r.Group("/admin", AuthRequired(), AdminOnly()){ users := admin.Group("/users") { users.GET("", listAdminUsers) users.DELETE("/:id", deleteUser) }}Context: Passing Data Between Handlers
Section titled “Context: Passing Data Between Handlers”// Set a value in middlewarec.Set("userID", 42)c.Set("user", &User{Name: "Alice"})
// Get a value in the next handleruserID, 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
Section titled “File Uploads”// Single filer.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 filesr.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
Section titled “HTML Templates”// Load templatesr.LoadHTMLGlob("templates/*") // All files in templates/r.LoadHTMLFiles("tmpl/a.html", "tmpl/b.html") // Specific files
// Render a templater.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "Home", "user": "Alice", })})
// Serve static filesr.Static("/assets", "./static") // Directory as staticr.StaticFile("/favicon.ico", "./static/favicon.ico") // Single filer.StaticFS("/files", http.Dir("./uploads")) // Custom fsCustom Error Handling & 404/405
Section titled “Custom Error Handling & 404/405”// Custom 404 handlerr.NoRoute(func(c *gin.Context) { c.JSON(404, gin.H{"error": "route not found"})})
// Custom 405 handlerr.NoMethod(func(c *gin.Context) { c.JSON(405, gin.H{"error": "method not allowed"})})
// Handle panic in middleware with Recoveryr.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
Section titled “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
Section titled “TLS / HTTPS”r.RunTLS(":443", "cert.pem", "key.pem") // HTTPS with cert filesr.RunAutoTLS(":443") // Auto TLS via Let's Encrypt (autocert)
// Or using http.Server directlysrv := &http.Server{ Addr: ":443", Handler: r, TLSConfig: tlsCfg, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,}srv.ListenAndServeTLS("cert.pem", "key.pem")Streaming Responses
Section titled “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) manuallyr.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
Section titled “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)
Section titled “Useful Built-in Middleware (from gin-contrib)”// CORS — github.com/gin-contrib/corsimport "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 IDr.Use(func(c *gin.Context) { c.Set("requestID", uuid.New().String()) c.Header("X-Request-ID", c.GetString("requestID")) c.Next()})Logger Configuration
Section titled “Logger Configuration”// Custom logger formatr.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 pathsr.Use(gin.LoggerWithConfig(gin.LoggerConfig{ SkipPaths: []string{"/health", "/metrics"},}))Common Patterns
Section titled “Common Patterns”// Health check endpointr.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"})})
// Versioned APIv1 := r.Group("/api/v1")v2 := r.Group("/api/v2")
// Dependency injection via closurefunc 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 earlyfunc 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
Section titled “Quick Reference — c *gin.Context Methods”// Request infoc.Request.Method // "GET", "POST", etc.c.FullPath() // Registered route: "/users/:id"c.ClientIP() // Best-guess client IPc.ContentType() // "application/json", etc.c.IsWebsocket() // true if Upgrade: websocket
// Response controlc.Status(202) // Set status code onlyc.Writer.Written() // true if response headers sentc.Writer.Status() // Current status codec.Writer.Size() // Bytes written so far
// Copy context for async usecp := c.Copy() // Safe to use in goroutinesgo func(ctx *gin.Context) { // use cp, never c in goroutines}(cp)