Gin

The most popular Go web framework. Optimized for fast and simple API development with Martini-like API.

GoFrameworkWeb DevelopmentHTTPLightweightHigh PerformanceRouter

Framework

Gin

Overview

Gin is a lightweight, high-performance HTTP web framework written in Go. It provides a Martini-like API while achieving up to 40x faster performance.

GitHub Star History

GitHub Star History for gin-gonic/gin

Details

Gin was developed in 2014 as an HTTP web framework for Go language. It adopts a zero-allocation router based on httprouter, minimizing memory allocation to achieve extremely high performance. While maintaining the usability of Martini, it has achieved significant performance improvements. With its simple API design and rich built-in features, it's optimized for developing RESTful APIs and microservices. It comes standard with features such as JSON/XML binding, automatic validation, middleware system, route grouping, and template rendering. Despite its small footprint, it provides enterprise-level functionality and excels particularly in API development and microservice architectures. Through the gin-contrib project, a rich middleware ecosystem is also available.

Pros and Cons

Pros

  • Extremely High Performance: Ultra-fast processing with zero-allocation router
  • Lightweight Design: Small footprint and minimal memory usage
  • Simple API: Intuitive and easy-to-understand Martini-like API
  • Rich Built-in Features: Standard binding, validation, and rendering capabilities
  • Powerful Middleware: Hierarchical middleware system and gin-contrib ecosystem
  • Crash Resilience: Server protection through built-in recovery
  • Active Community: Active development and community support

Cons

  • Go Language Dependency: Go-specific learning curve and development environment constraints
  • Feature Simplicity: Limited functionality compared to full-stack frameworks
  • Convention Constraints: Flexibility may be limited by performance-oriented conventions
  • Debug Information: Limited detailed debug information due to performance priority
  • Template Limitations: Only basic template functionality provided

Key Links

Code Examples

Hello World

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

func main() {
	// Create Gin instance (default comes with Logger + Recovery middleware)
	r := gin.Default()

	// GET /hello route
	r.GET("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello, World!")
	})

	// GET / route
	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Welcome to Gin!",
			"version": "v1.10.0",
		})
	})

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

Routing and Parameters

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

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

	// Basic routes
	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Gin API Server",
		})
	})

	// Path parameters
	r.GET("/users/:id", func(c *gin.Context) {
		id := c.Param("id")
		c.JSON(http.StatusOK, gin.H{
			"user_id": id,
			"name":    "User " + id,
		})
	})

	// Wildcard
	r.GET("/files/*filepath", func(c *gin.Context) {
		filepath := c.Param("filepath")
		c.JSON(http.StatusOK, gin.H{
			"filepath": filepath,
			"message":  "File: " + filepath,
		})
	})

	// Query parameters
	r.GET("/search", func(c *gin.Context) {
		query := c.Query("q")
		page := c.DefaultQuery("page", "1")
		
		c.JSON(http.StatusOK, gin.H{
			"query":   query,
			"page":    page,
			"results": []string{"Result 1", "Result 2", "Result 3"},
		})
	})

	// HTTP methods
	r.POST("/users", createUser)
	r.PUT("/users/:id", updateUser)
	r.DELETE("/users/:id", deleteUser)

	r.Run(":8080")
}

func createUser(c *gin.Context) {
	c.JSON(http.StatusCreated, gin.H{
		"message": "User created successfully",
	})
}

func updateUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " updated successfully",
	})
}

func deleteUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " deleted successfully",
	})
}

JSON Binding and Validation

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
)

type User struct {
	Name  string `json:"name" binding:"required,min=2,max=50"`
	Email string `json:"email" binding:"required,email"`
	Age   int    `json:"age" binding:"min=0,max=120"`
}

type LoginRequest struct {
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required,min=6"`
}

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

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

		// Bind JSON
		if err := c.ShouldBindJSON(&user); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		// User processing (database save, etc.)
		// ...

		c.JSON(http.StatusCreated, gin.H{
			"message": "User created successfully",
			"user":    user,
		})
	})

	// URI binding
	r.GET("/users/:id/:name", func(c *gin.Context) {
		var uri struct {
			ID   int    `uri:"id" binding:"required"`
			Name string `uri:"name" binding:"required"`
		}

		if err := c.ShouldBindUri(&uri); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"id":   uri.ID,
			"name": uri.Name,
		})
	})

	// Form binding
	r.POST("/login", func(c *gin.Context) {
		var login LoginRequest

		if err := c.ShouldBindWith(&login, binding.Form); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		// Authentication logic
		if login.Email == "[email protected]" && login.Password == "password123" {
			c.JSON(http.StatusOK, gin.H{
				"message": "Login successful",
				"token":   "jwt_token_here",
			})
		} else {
			c.JSON(http.StatusUnauthorized, gin.H{
				"error": "Authentication failed",
			})
		}
	})

	r.Run(":8080")
}

Using Middleware

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

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

func main() {
	// Gin instance without middleware
	r := gin.New()

	// Global middleware
	r.Use(gin.Logger())
	r.Use(gin.Recovery())

	// Custom middleware
	r.Use(customLogger())
	r.Use(corsMiddleware())

	// Authentication middleware
	r.Use("/api", authMiddleware())

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

	// Protected API routes
	api := r.Group("/api")
	{
		api.GET("/users", func(c *gin.Context) {
			userID := c.GetString("userID")
			c.JSON(http.StatusOK, gin.H{
				"message": "User list",
				"user_id": userID,
			})
		})

		api.POST("/posts", func(c *gin.Context) {
			c.JSON(http.StatusCreated, gin.H{
				"message": "Post created successfully",
			})
		})
	}

	// Admin-only routes
	admin := r.Group("/admin")
	admin.Use(adminMiddleware())
	{
		admin.GET("/dashboard", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{
				"message": "Admin dashboard",
			})
		})
	}

	r.Run(":8080")
}

func customLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path

		// Process request
		c.Next()

		// Log output
		latency := time.Since(start)
		method := c.Request.Method
		statusCode := c.Writer.Status()

		fmt.Printf("[GIN] %s %s %d %v\n", method, path, statusCode, latency)
	}
}

func corsMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	}
}

func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.GetHeader("Authorization")

		if token == "" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"error": "Authentication token required",
			})
			c.Abort()
			return
		}

		// Token verification (in actual implementation, use JWT, etc.)
		if token != "Bearer valid_token" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"error": "Invalid token",
			})
			c.Abort()
			return
		}

		// Set user info in context
		c.Set("userID", "123")
		c.Next()
	}
}

func adminMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		role := c.GetHeader("X-User-Role")

		if role != "admin" {
			c.JSON(http.StatusForbidden, gin.H{
				"error": "Admin privileges required",
			})
			c.Abort()
			return
		}

		c.Next()
	}
}

Route Groups

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

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

	// Basic routes
	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Gin API Server",
		})
	})

	// API version 1
	v1 := r.Group("/api/v1")
	{
		users := v1.Group("/users")
		{
			users.GET("/", getUsersV1)
			users.POST("/", createUserV1)
			users.GET("/:id", getUserV1)
			users.PUT("/:id", updateUserV1)
			users.DELETE("/:id", deleteUserV1)
		}

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

	// API version 2 (with new features)
	v2 := r.Group("/api/v2")
	v2.Use(apiVersionMiddleware("v2"))
	{
		users := v2.Group("/users")
		{
			users.GET("/", getUsersV2)
			users.POST("/", createUserV2)
			users.GET("/:id", getUserV2)
			users.PUT("/:id", updateUserV2)
			users.DELETE("/:id", deleteUserV2)
			users.GET("/:id/profile", getUserProfileV2) // V2 new feature
		}
	}

	// Admin API
	admin := r.Group("/admin")
	admin.Use(authMiddleware())
	{
		admin.GET("/stats", getStats)
		admin.GET("/logs", getLogs)
		
		users := admin.Group("/users")
		{
			users.GET("/", getAllUsersAdmin)
			users.POST("/:id/ban", banUser)
			users.DELETE("/:id", deleteUserAdmin)
		}
	}

	r.Run(":8080")
}

func apiVersionMiddleware(version string) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Header("API-Version", version)
		c.Next()
	}
}

// V1 handlers
func getUsersV1(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"version": "v1",
		"users": []gin.H{
			{"id": 1, "name": "User 1"},
			{"id": 2, "name": "User 2"},
		},
	})
}

func createUserV1(c *gin.Context) {
	c.JSON(http.StatusCreated, gin.H{
		"message": "User created (V1)",
	})
}

func getUserV1(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"version": "v1",
		"user": gin.H{
			"id":   id,
			"name": "User " + id,
		},
	})
}

func updateUserV1(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " updated (V1)",
	})
}

func deleteUserV1(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " deleted (V1)",
	})
}

// V2 handlers (with extended features)
func getUsersV2(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"version": "v2",
		"users": []gin.H{
			{"id": 1, "name": "User 1", "email": "[email protected]", "active": true},
			{"id": 2, "name": "User 2", "email": "[email protected]", "active": false},
		},
	})
}

func createUserV2(c *gin.Context) {
	c.JSON(http.StatusCreated, gin.H{
		"message": "User created (V2 - with extended features)",
	})
}

func getUserV2(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"version": "v2",
		"user": gin.H{
			"id":     id,
			"name":   "User " + id,
			"email":  "user" + id + "@example.com",
			"active": true,
		},
	})
}

func updateUserV2(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " updated (V2)",
	})
}

func deleteUserV2(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " deleted (V2)",
	})
}

func getUserProfileV2(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"user_id": id,
		"profile": gin.H{
			"bio":      "Profile of User " + id,
			"location": "Tokyo, Japan",
			"website":  "https://example.com",
		},
	})
}

// Other handlers
func getPostsV1(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"posts": []gin.H{
			{"id": 1, "title": "Post 1"},
			{"id": 2, "title": "Post 2"},
		},
	})
}

func createPostV1(c *gin.Context) {
	c.JSON(http.StatusCreated, gin.H{
		"message": "Post created successfully",
	})
}

func getPostV1(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"post": gin.H{
			"id":    id,
			"title": "Post " + id,
		},
	})
}

func getStats(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"stats": gin.H{
			"users": 150,
			"posts": 1200,
		},
	})
}

func getLogs(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"logs": []string{
			"Log entry 1",
			"Log entry 2",
		},
	})
}

func getAllUsersAdmin(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "All users management",
	})
}

func banUser(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " banned",
	})
}

func deleteUserAdmin(c *gin.Context) {
	id := c.Param("id")
	c.JSON(http.StatusOK, gin.H{
		"message": "User " + id + " deleted (admin)",
	})
}

func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.GetHeader("Authorization")
		if token != "Bearer admin_token" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"error": "Admin authentication required",
			})
			c.Abort()
			return
		}
		c.Next()
	}
}

Error Handling

package main

import (
	"errors"
	"fmt"
	"net/http"
	"time"
	"github.com/gin-gonic/gin"
)

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

	// Custom error handling middleware
	r.Use(errorHandler())

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

	// 400 error
	r.GET("/bad-request", func(c *gin.Context) {
		c.Error(errors.New("bad request"))
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "Bad Request",
		})
	})

	// Custom error
	r.GET("/custom-error", func(c *gin.Context) {
		err := errors.New("custom error occurred")
		c.Error(err)
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
	})

	// Panic test
	r.GET("/panic", func(c *gin.Context) {
		panic("panic occurred")
	})

	// Validation error
	r.POST("/validate", func(c *gin.Context) {
		var input struct {
			Name  string `json:"name" binding:"required"`
			Email string `json:"email" binding:"required,email"`
		}

		if err := c.ShouldBindJSON(&input); err != nil {
			c.Error(err)
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
				"error":   "Validation error",
				"details": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"message": "Validation successful",
			"data":    input,
		})
	})

	// Database error simulation
	r.GET("/db-error", func(c *gin.Context) {
		err := simulateDatabaseError()
		if err != nil {
			c.Error(err)
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
				"error": "Database error occurred",
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"message": "Database operation successful",
		})
	})

	r.Run(":8080")
}

func errorHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		// Handle errors if any
		for _, err := range c.Errors {
			// Error logging
			fmt.Printf("Error: %s\n", err.Error())
			
			// Branch by error type
			switch err.Type {
			case gin.ErrorTypeBind:
				// Binding error
				c.JSON(http.StatusBadRequest, gin.H{
					"error":     "Request format is incorrect",
					"details":   err.Error(),
					"timestamp": time.Now().Format(time.RFC3339),
				})
			case gin.ErrorTypePublic:
				// Public error
				c.JSON(http.StatusInternalServerError, gin.H{
					"error":     "Internal server error",
					"timestamp": time.Now().Format(time.RFC3339),
				})
			default:
				// Other errors
				c.JSON(http.StatusInternalServerError, gin.H{
					"error":     "Unexpected error occurred",
					"timestamp": time.Now().Format(time.RFC3339),
				})
			}
		}
	}
}

func simulateDatabaseError() error {
	// Simulate database error
	return errors.New("connection timeout")
}

File Upload and WebSocket

package main

import (
	"fmt"
	"net/http"
	"path/filepath"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true // Implement proper origin check in production
	},
}

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

	// Static file serving
	r.Static("/static", "./static")

	// Single file upload
	r.POST("/upload", func(c *gin.Context) {
		file, err := c.FormFile("file")
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": "Failed to get file",
			})
			return
		}

		// File size check (10MB limit)
		if file.Size > 10<<20 {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": "File too large (max 10MB)",
			})
			return
		}

		// Save file
		filename := filepath.Base(file.Filename)
		if err := c.SaveUploadedFile(file, "./uploads/"+filename); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{
				"error": "Failed to save file",
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"message":  "File upload successful",
			"filename": filename,
			"size":     file.Size,
		})
	})

	// Multiple file upload
	r.POST("/upload/multiple", func(c *gin.Context) {
		form, _ := c.MultipartForm()
		files := form.File["files"]

		var uploadedFiles []string

		for _, file := range files {
			filename := filepath.Base(file.Filename)
			if err := c.SaveUploadedFile(file, "./uploads/"+filename); err == nil {
				uploadedFiles = append(uploadedFiles, filename)
			}
		}

		c.JSON(http.StatusOK, gin.H{
			"message":        "File upload completed",
			"uploaded_files": uploadedFiles,
			"count":          len(uploadedFiles),
		})
	})

	// WebSocket endpoint
	r.GET("/ws", func(c *gin.Context) {
		conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
		if err != nil {
			fmt.Printf("WebSocket upgrade error: %v\n", err)
			return
		}
		defer conn.Close()

		fmt.Println("WebSocket connection established")

		for {
			// Read message
			messageType, message, err := conn.ReadMessage()
			if err != nil {
				fmt.Printf("Read error: %v\n", err)
				break
			}

			fmt.Printf("Received: %s\n", message)

			// Echo back
			if err := conn.WriteMessage(messageType, message); err != nil {
				fmt.Printf("Write error: %v\n", err)
				break
			}
		}
	})

	// Broadcast WebSocket for chat
	r.GET("/ws/chat", func(c *gin.Context) {
		handleChatWebSocket(c)
	})

	r.Run(":8080")
}

// Chat WebSocket handler
func handleChatWebSocket(c *gin.Context) {
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		return
	}
	defer conn.Close()

	// User info
	userID := c.Query("user_id")
	if userID == "" {
		userID = "anonymous"
	}

	fmt.Printf("User %s joined the chat\n", userID)

	for {
		var msg struct {
			Type    string `json:"type"`
			Message string `json:"message"`
			UserID  string `json:"user_id"`
		}

		if err := conn.ReadJSON(&msg); err != nil {
			fmt.Printf("JSON read error: %v\n", err)
			break
		}

		msg.UserID = userID

		// Echo message back (in actual implementation, broadcast to other clients)
		if err := conn.WriteJSON(msg); err != nil {
			fmt.Printf("JSON write error: %v\n", err)
			break
		}

		fmt.Printf("Chat message: %s from %s\n", msg.Message, msg.UserID)
	}

	fmt.Printf("User %s left the chat\n", userID)
}