Gin
The most popular Go web framework. Optimized for fast and simple API development with Martini-like API.
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
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
- Gin Official Site
- Gin Official Documentation
- Gin GitHub Repository
- Gin Middleware (gin-contrib)
- Gin Examples
- Gin Community
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)
}