Skip to main content

Gin Framework

Gin is the most widely used HTTP web framework in Go. It uses a high-performance router based on httprouter, and its rich middleware ecosystem and clean API allow you to build REST APIs quickly. It delivers performance close to net/http in benchmarks while offering significantly higher development productivity.

Installation and Basic Usage

go get github.com/gin-gonic/gin
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
// gin.Default(): includes Logger + Recovery middleware
// gin.New(): pure engine with no middleware
r := gin.Default()

r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})

// Start server (default :8080)
r.Run(":8080")
}

Routing

package main

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
)

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

// Register routes by HTTP method
r.GET("/users", listUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.PATCH("/users/:id", patchUser)
r.DELETE("/users/:id", deleteUser)
r.OPTIONS("/users", optionsUsers)
r.Any("/any", anyHandler) // handles all methods

// Path parameters
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // extract :id value
c.String(http.StatusOK, "User ID: %s", id)
})

// Wildcard parameter (* prefix, matches the rest of the path)
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "File path: %s", filepath)
})

// Query parameters
r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // single query parameter
page := c.DefaultQuery("page", "1") // with default value
tags := c.QueryArray("tag") // multiple values (tag=go&tag=api)
fmt.Printf("q=%s, page=%s, tags=%v\n", q, page, tags)
c.JSON(http.StatusOK, gin.H{"q": q, "page": page, "tags": tags})
})

r.Run(":8080")
}

func listUsers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"users": []string{}}) }
func createUser(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"id": 1}) }
func updateUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"updated": true}) }
func patchUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"patched": true}) }
func deleteUser(c *gin.Context) { c.Status(http.StatusNoContent) }
func optionsUsers(c *gin.Context) { c.Status(http.StatusNoContent) }
func anyHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": c.Request.Method}) }

Router Groups

package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

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

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

posts := v1.Group("/posts")
{
posts.GET("", listPostsV1)
posts.POST("", createPostV1)
}
}

// v2 group (with auth middleware applied)
v2 := r.Group("/api/v2")
v2.Use(authMiddleware())
{
v2.GET("/users", listUsersV2)
}

r.Run(":8080")
}

func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "UNAUTHORIZED", "message": "Authentication required"},
})
return
}
c.Next()
}
}

func listUsersV1(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": "v1"}) }
func createUserV1(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"version": "v1"}) }
func getUserV1(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"id": c.Param("id")}) }
func listPostsV1(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"posts": []any{}}) }
func createPostV1(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"version": "v1"}) }
func listUsersV2(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": "v2"}) }

Request Binding and Validation

package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

// CreateUserRequest struct for user creation request
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=1,max=50"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"omitempty,min=1,max=150"`
Role string `json:"role" binding:"omitempty,oneof=admin user guest"`
}

// QueryRequest for query parameter binding
type QueryRequest struct {
Page int `form:"page" binding:"omitempty,min=1"`
Limit int `form:"limit" binding:"omitempty,min=1,max=100"`
Q string `form:"q"`
}

// PathRequest for path parameter binding
type PathRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

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

// JSON binding
r.POST("/users", func(c *gin.Context) {
var req CreateUserRequest

// ShouldBind: returns error (BindJSON: automatically sends 400 on error)
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": gin.H{
"code": "VALIDATION_ERROR",
"message": err.Error(),
},
})
return
}

c.JSON(http.StatusCreated, gin.H{"data": req})
})

// Query parameter binding
r.GET("/users", func(c *gin.Context) {
var q QueryRequest
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if q.Page == 0 { q.Page = 1 }
if q.Limit == 0 { q.Limit = 20 }
c.JSON(http.StatusOK, gin.H{"page": q.Page, "limit": q.Limit, "q": q.Q})
})

// Path parameter binding
r.GET("/users/:id", func(c *gin.Context) {
var p PathRequest
if err := c.ShouldBindUri(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
c.JSON(http.StatusOK, gin.H{"id": p.ID})
})

r.Run(":8080")
}

Middleware

package main

import (
"log/slog"
"net/http"
"os"
"time"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
)

// RequestIDMiddleware injects a request ID
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
c.Set("request_id", id)
c.Header("X-Request-ID", id)
c.Next() // execute next handler/middleware
}
}

// SlogMiddleware structured logging with slog
func SlogMiddleware(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
logger.Info("request",
slog.String("request_id", c.GetString("request_id")),
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.Int("status", c.Writer.Status()),
slog.Duration("latency", time.Since(start)),
slog.String("ip", c.ClientIP()),
)
}
}

// c.Abort() vs c.Next()
// c.Next(): executes the next handler, then returns to the current handler
// c.Abort(): stops execution of subsequent handlers (current handler continues)
// c.AbortWithStatus()/c.AbortWithStatusJSON(): Abort + send response at the same time

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

r := gin.New() // start with no middleware
r.Use(
gin.CustomRecovery(func(c *gin.Context, err any) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": gin.H{"code": "INTERNAL_ERROR", "message": "An internal server error occurred"},
})
}),
RequestIDMiddleware(),
SlogMiddleware(logger),
)

r.GET("/users", func(c *gin.Context) {
// Retrieve value stored with c.Set
requestID := c.GetString("request_id")
c.JSON(http.StatusOK, gin.H{
"request_id": requestID,
"users": []any{},
})
})

r.Run(":8080")
}

Practical Example: User CRUD API with JWT Authentication

package main

import (
"net/http"
"strconv"
"sync"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)

var secret = []byte("my-secret-key")

type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}

type UserStore struct {
mu sync.RWMutex
users map[int64]*User
nextID int64
}

func NewUserStore() *UserStore {
s := &UserStore{users: make(map[int64]*User), nextID: 1}
// Initial seed data for testing
s.users[1] = &User{ID: 1, Name: "Admin", Email: "admin@example.com", CreatedAt: time.Now()}
s.nextID = 2
return s
}

func (s *UserStore) List() []*User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]*User, 0, len(s.users))
for _, u := range s.users {
users = append(users, u)
}
return users
}

func (s *UserStore) Get(id int64) (*User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}

func (s *UserStore) Create(name, email string) *User {
s.mu.Lock()
defer s.mu.Unlock()
u := &User{ID: s.nextID, Name: name, Email: email, CreatedAt: time.Now()}
s.users[s.nextID] = u
s.nextID++
return u
}

func (s *UserStore) Delete(id int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
return false
}
delete(s.users, id)
return true
}

// JWT token issuance (login)
func loginHandler(c *gin.Context) {
var body struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "BAD_REQUEST", "message": err.Error()}})
return
}

// In production, verify user from DB
if body.Email != "admin@example.com" || body.Password != "password" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "INVALID_CREDENTIALS", "message": "Invalid email or password"},
})
return
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 1,
"email": body.Email,
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
tokenString, _ := token.SignedString(secret)
c.JSON(http.StatusOK, gin.H{"token": tokenString})
}

// JWT verification middleware
func jwtAuth() gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if len(auth) < 8 || auth[:7] != "Bearer " {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "UNAUTHORIZED", "message": "Bearer token required"},
})
return
}
claims := jwt.MapClaims{}
t, err := jwt.ParseWithClaims(auth[7:], claims, func(t *jwt.Token) (any, error) {
return secret, nil
})
if err != nil || !t.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "INVALID_TOKEN", "message": "Invalid token"},
})
return
}
c.Set("user_id", claims["user_id"])
c.Next()
}
}

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

// Public routes
r.POST("/api/login", loginHandler)

// Routes requiring authentication
api := r.Group("/api/v1")
api.Use(jwtAuth())
{
users := api.Group("/users")
users.GET("", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": store.List()})
})
users.POST("", func(c *gin.Context) {
var body struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": gin.H{"code": "VALIDATION_ERROR", "message": err.Error()},
})
return
}
c.JSON(http.StatusCreated, gin.H{"data": store.Create(body.Name, body.Email)})
})
users.GET("/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
u, ok := store.Get(id)
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{"code": "NOT_FOUND", "message": "User not found"},
})
return
}
c.JSON(http.StatusOK, gin.H{"data": u})
})
users.DELETE("/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if !store.Delete(id) {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{"code": "NOT_FOUND", "message": "User not found"},
})
return
}
c.Status(http.StatusNoContent)
})
}

r.Run(":8080")
}

Expert Tips

1. gin.New() vs gin.Default(): In production, start with gin.New() and add only the middleware you need. The default logger in gin.Default() does not support structured logging.

2. ShouldBind vs Bind: Bind automatically writes a 400 response and calls c.Abort() on failure. Always use ShouldBind when you need a custom error response.

3. Stop middleware chain with c.Abort(): Simply returning on authentication failure may still allow subsequent handlers to run. Always use c.AbortWithStatusJSON().

4. gin.H is an alias for map[string]any: It is short and convenient, but for complex responses, define a struct for type safety.

5. Release mode: Setting the GIN_MODE=release environment variable removes unnecessary debug logs and improves performance.