Expert guidance for building HTTP web applications and APIs with the Gin web framework in Go. Use when working with Gin routers, middleware, route handlers, request binding, response rendering, template engines, authentication, or any HTTP server development using github.com/gin-gonic/gin.
Expert guidance for building modern HTTP web applications and REST APIs using the Gin web framework in Go.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default() // Creates router with Logger and Recovery middleware
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
r.Run(":8080") // Listen and serve on 0.0.0.0:8080
}
Default router (includes Logger + Recovery middleware):
r := gin.Default()
New router (no middleware):
r := gin.New()
Custom middleware:
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(customMiddleware())
r.GET("/get", getHandler)
r.POST("/post", postHandler)
r.PUT("/put", putHandler)
r.DELETE("/delete", deleteHandler)
r.PATCH("/patch", patchHandler)
r.HEAD("/head", headHandler)
r.OPTIONS("/options", optionsHandler)
// Match /user/john
r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// Match /user/john/books
r.GET("/user/:name/:action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
c.String(http.StatusOK, "%s is %s", name, action)
})
// Match /static/css/style.css or /static/js/app.js
r.GET("/static/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, filepath)
})
v1 := r.Group("/v1")
{
v1.GET("/users", listUsers)
v1.POST("/users", createUser)
v1.GET("/users/:id", getUser)
}
v2 := r.Group("/v2")
{
v2.GET("/users", listUsersV2)
v2.POST("/users", createUserV2)
}
r.GET("/search", func(c *gin.Context) {
// Default value if not provided
query := c.DefaultQuery("q", "default")
// Get query parameter (empty string if not exists)
page := c.Query("page")
// Get query parameter with existence check
if name, ok := c.GetQuery("name"); ok {
// name exists
}
})
r.POST("/form", func(c *gin.Context) {
// Get form value
username := c.PostForm("username")
// Get with default value
password := c.DefaultPostForm("password", "guest")
// Get with existence check
if email, ok := c.GetPostForm("email"); ok {
// email exists
}
})
type Login struct {
User string `json:"user" binding:"required"`
Password string `json:"password" binding:"required"`
}
r.POST("/login", func(c *gin.Context) {
var json Login
// Bind JSON - Returns 400 if validation fails
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": json.User})
})
type Query struct {
Name string `form:"name" binding:"required"`
Age int `form:"age" binding:"gte=0"`
}
r.GET("/query", func(c *gin.Context) {
var query Query
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, query)
})
type User struct {
ID int `uri:"id" binding:"required"`
}
r.GET("/users/:id", func(c *gin.Context) {
var user User
if err := c.ShouldBindUri(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": user.ID})
})
Single file:
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Save file
c.SaveUploadedFile(file, "uploads/" + file.Filename)
c.JSON(http.StatusOK, gin.H{"filename": file.Filename})
})
Multiple files:
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.JSON(http.StatusOK, gin.H{"count": len(files)})
})
// gin.H is shortcut for map[string]interface{}
c.JSON(http.StatusOK, gin.H{
"message": "success",
"data": data,
})
// Or use struct
c.JSON(http.StatusOK, User{Name: "John", Age: 30})
// Pretty JSON with indentation
c.IndentedJSON(http.StatusOK, data)
// Secure JSON (prevents JSON hijacking)
c.SecureJSON(http.StatusOK, data)
// ASCII-only JSON
c.AsciiJSON(http.StatusOK, data)
// Pure JSON (don't replace special chars)
c.PureJSON(http.StatusOK, data)
c.XML(http.StatusOK, gin.H{"message": "success"})
c.YAML(http.StatusOK, gin.H{"message": "success"})
c.TOML(http.StatusOK, gin.H{"message": "success"})
c.String(http.StatusOK, "Hello %s", name)
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Main Page",
})
})
// Serve file
c.File("/path/to/file.pdf")
// Serve file as attachment (download)
c.FileAttachment("/path/to/file.pdf", "download.pdf")
// Serve from filesystem
c.FileFromFS("index.html", http.Dir("./public"))
// HTTP redirect
c.Redirect(http.StatusMovedPermanently, "https://google.com")
// Router redirect
c.Request.URL.Path = "/new-path"
r.HandleContext(c)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "authorization required",
})
return
}
// Continue to next handler
c.Next()
}
}
// Apply globally
r.Use(AuthRequired())
// Apply to route group
authorized := r.Group("/api")
authorized.Use(AuthRequired())
{
authorized.GET("/users", listUsers)
}
// Apply to specific route
r.GET("/protected", AuthRequired(), protectedHandler)
func MyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Before request
c.Next() // Execute remaining handlers
// After request
}
}
func AbortMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !authorized {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Next()
}
}
Basic Auth:
authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"admin": "secret",
"user": "password",
}))
CORS (requires github.com/gin-contrib/cors):
import "github.com/gin-contrib/cors"
r.Use(cors.Default())
// Or custom config
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// Store value in context
c.Set("user", user)
// Retrieve value
if val, exists := c.Get("user"); exists {
user := val.(User)
}
// MustGet (panics if not exists)
user := c.MustGet("user").(User)
// Client IP
ip := c.ClientIP()
// Content type
contentType := c.ContentType()
// Request header
userAgent := c.GetHeader("User-Agent")
// Check if WebSocket
isWebsocket := c.IsWebsocket()
// Set cookie
c.SetCookie(
"session", // name
"value", // value
3600, // max age (seconds)
"/", // path
"localhost", // domain
false, // secure
true, // httpOnly
)
// Get cookie
value, err := c.Cookie("session")
// Read body bytes (can only be read once)
bodyBytes, _ := c.GetRawData()
// To allow multiple reads, bind to GetRawData and bind again
c.Set(gin.BodyBytesKey, bodyBytes)
r.GET("/error", func(c *gin.Context) {
// Attach error to context
c.Error(errors.New("something went wrong"))
// Abort with error
c.AbortWithError(http.StatusInternalServerError, errors.New("error"))
// Abort with JSON
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "invalid input",
})
})
r.Use(func(c *gin.Context) {
c.Next()
// Check for errors after handlers executed
if len(c.Errors) > 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"errors": c.Errors,
})
}
})
// Load all templates
r.LoadHTMLGlob("templates/**/*")
// Load specific files
r.LoadHTMLFiles("templates/index.html", "templates/about.html")
r.Delims("{[{", "}]}")
import "html/template"
r.SetFuncMap(template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
})
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Home",
"user": user,
})
})
// Serve single file
r.StaticFile("/favicon.ico", "./resources/favicon.ico")
// Serve directory
r.Static("/assets", "./assets")
// Serve from embedded FS (Go 1.16+)
r.StaticFS("/public", http.FS(embeddedFS))
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
r := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "pong", w.Body.String())
}
import (
"context"
"net/http"
"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: %s\n", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
}
import "github.com/go-playground/validator/v10"
type Booking struct {
CheckIn time.Time `binding:"required,bookabledate"`
CheckOut time.Time `binding:"required,gtfield=CheckIn"`
}
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
return date.After(time.Now())
}
return false
}
// Register custom validator
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
func main() {
r1 := gin.Default()
r2 := gin.Default()
s1 := &http.Server{Addr: ":8080", Handler: r1}
s2 := &http.Server{Addr: ":8081", Handler: r2}
g := new(errgroup.Group)
g.Go(func() error { return s1.ListenAndServe() })
g.Go(func() error { return s2.ListenAndServe() })
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
import "github.com/gin-gonic/autotls"
func main() {
r := gin.Default()
// Automatically obtain TLS certificates from Let's Encrypt
log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
}
func main() {
r := gin.Default()
v1 := r.Group("/api/v1")
{
v1.GET("/users", v1GetUsers)
v1.POST("/users", v1CreateUser)
}
v2 := r.Group("/api/v2")
{
v2.GET("/users", v2GetUsers)
v2.POST("/users", v2CreateUser)
}
r.Run()
}
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}
func Error(c *gin.Context, code int, message string) {
c.JSON(code, Response{
Code: code,
Message: message,
})
}
type PaginationQuery struct {
Page int `form:"page" binding:"min=1"`
PageSize int `form:"page_size" binding:"min=1,max=100"`
}
r.GET("/users", func(c *gin.Context) {
var query PaginationQuery
query.Page = 1
query.PageSize = 10
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
offset := (query.Page - 1) * query.PageSize
users := getUsersFromDB(query.PageSize, offset)
c.JSON(http.StatusOK, gin.H{
"page": query.Page,
"page_size": query.PageSize,
"data": users,
})
})
// Set mode programmatically
gin.SetMode(gin.ReleaseMode) // Production
gin.SetMode(gin.DebugMode) // Development
gin.SetMode(gin.TestMode) // Testing
// Or via environment variable
// export GIN_MODE=release
gin.DisableConsoleColor()
import (
"io"
"os"
)
// Write logs to file
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
// Disable logs
gin.DefaultWriter = io.Discard
r := gin.Default()
r.SetTrustedProxies([]string{"192.168.1.0/24"})
gin.New() instead of gin.Default() if you don't need logger/recovery middlewaregin.DisableConsoleColor()gin.SetMode(gin.ReleaseMode)c.ShouldBind* instead of c.Bind* for better error handling controlc.Copy() for goroutinesSee the references/examples.md file for additional working examples covering:
For complete API reference, see references/api.md.