What is the Gin framework?
Gin is an HTTP web framework written in Go. Its design philosophy is simple: fast, concise, and efficient. Compared to other frameworks, what are Gin’s advantages?
First, let’s talk about performance! Gin is based on httprouter, so route lookup is extremely fast. Official tests show that it’s several times faster than other Go frameworks (this performance difference is really significant).
Secondly, it’s simple and easy to use. Gin’s API is designed to be quite intuitive; you can basically figure out how to use it at a glance. Unlike some frameworks, its learning curve is so steep that it makes you want to give up.
Finally, it boasts a rich feature set. Middleware, JSON binding, route grouping, static file service… it has all the essential features, and they are all very easy to use.
Environmental preparation and installation
Before you begin, make sure your Go version is 1.13 or higher (1.16+ recommended). The method for checking is simple:
go version
If your version is too low, upgrade it immediately. Then create a new project:
mkdir gin-demo
cd gin-demo
go mod init gin-demo
Install the Gin framework:
go get github.com/gin-gonic/gin
This step may take a while, please be patient. If your network is poor, you can consider configuring a Go proxy.
My first Gin application: Hello World
Let’s start with the simplest example. Create a main.gofile:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// Create a Gin engine
r := gin.Default()
// Define a GET route
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello, Gin!",
})
})
// Start the server, default port 8080
r.Run()
}
Run the program:
go run main.go
Open your browser and access it http://localhost:8080; you should see a JSON response. Isn’t that super simple?
Routing basics: GET, POST, PUT, DELETE
Web applications rely on various HTTP methods. Gin provides excellent support for these:
func main() {
r := gin.Default()
// GET request
r.GET("/users", getUsers)
// POST request
r.POST("/users", createUser)
// PUT request
r.PUT("/users/:id", updateUser)
// DELETE request
r.DELETE("/users/:id", deleteUser)
r.Run()
}
func getUsers(c *gin.Context) {
c.JSON(200, gin.H{"message": "Get user list"})
}
func createUser(c *gin.Context) {
c.JSON(200, gin.H{"message": "Create user"})
}
func updateUser(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"message": "Update user", "id": id})
}
func deleteUser(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"message": "Delete user", "id": id})
}
Note that :id; this is the path parameter. Gin will automatically parse it and c.Param("id")provide it to you.
Process request parameters
In actual development, we often need to handle various parameters. Gin provides many convenient methods:
Query parameters
r.GET("/search", func(c *gin.Context) {
// Get query parameters
keyword := c.Query("keyword")
page := c.DefaultQuery("page", "1") // Provide default values
c.JSON(200, gin.H{
"keyword": keyword,
"page": page,
})
})
You’ll see the results once you visit the site /search?keyword=gin&page=2.
Form data
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
// Simple verification
if username == "admin" && password == "123456" {
c.JSON(200, gin.H{"status": "success"})
} else {
c.JSON(401, gin.H{"status": "failed"})
}
})
JSON data binding
This feature is incredibly useful! Gin can automatically bind the JSON request body to a struct:
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"min=1,max=120"`
}
r.POST("/users", func(c *gin.Context) {
var user User
// Bind JSON to a struct
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Processing user data
c.JSON(200, gin.H{"message": "User created successfully", "user": user})
})
Those bindingtags are very useful! They provide automatic verification functionality.
Middleware: Making functionality more powerful
Middleware is one of Gin’s most powerful features. You can use it to handle common logic such as authentication, logging, and cross-domain issues.
Built-in middleware
func main() {
r := gin.Default() // Already includes Logger and Recovery middleware
// Or start from new
// r := gin.New()
// r.Use(gin.Logger())
// r.Use(gin.Recovery())
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello"})
})
r.Run()
}
Custom Middleware
Write a simple authentication middleware:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "Lack of authentication token"})
c.Abort() // Prevent further execution
return
}
// Verify the validity of the token
if token != "Bearer valid-token" {
c.JSON(401, gin.H{"error": "Invalid token"})
c.Abort()
return
}
// Continue to execute the next processor
c.Next()
}
}
func main() {
r := gin.Default()
// Public routing
r.GET("/public", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "public interface"})
})
// Routes that require authentication
authorized := r.Group("/api")
authorized.Use(AuthMiddleware())
{
authorized.GET("/profile", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "User profile"})
})
authorized.POST("/upload", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "File upload"})
})
}
r.Run()
}
Route grouping: Makes the code clearer
Routing groups come in handy as your application grows:
func main() {
r := gin.Default()
// API v1 grouping
v1 := r.Group("/api/v1")
{
v1.GET("/users", getUsersV1)
v1.POST("/users", createUserV1)
}
// API v2 grouping
v2 := r.Group("/api/v2")
{
v2.GET("/users", getUsersV2)
v2.POST("/users", createUserV2)
}
// Administrator routing grouping
admin := r.Group("/admin")
admin.Use(AdminAuthMiddleware()) // Effective only for admin groups
{
admin.GET("/dashboard", getDashboard)
admin.DELETE("/users/:id", deleteUser)
}
r.Run()
}
Organizing the code this way makes the structure much clearer!
Practical Exercise: Building a Simple TODO API
Let’s combine what we’ve learned so far to build a complete TODO application:
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Todo struct {
ID int `json:"id"`
Title string `json:"title" binding:"required"`
Completed bool `json:"completed"`
}
// Simple memory storage
var todos []Todo
var nextID = 1
func main() {
r := gin.Default()
// Middleware: Add CORS support
r.Use(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
c.Header("Access-Control-Allow-Headers", "Content-Type")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
// Route definition
api := r.Group("/api")
{
api.GET("/todos", getTodos)
api.POST("/todos", createTodo)
api.PUT("/todos/:id", updateTodo)
api.DELETE("/todos/:id", deleteTodo)
}
r.Run(":8080")
}
func getTodos(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": todos,
"count": len(todos),
})
}
func createTodo(c *gin.Context) {
var newTodo Todo
if err := c.ShouldBindJSON(&newTodo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newTodo.ID = nextID
nextID++
newTodo.Completed = false
todos = append(todos, newTodo)
c.JSON(http.StatusCreated, gin.H{
"message": "Todo successfully created",
"data": newTodo,
})
}
func updateTodo(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
var updateData Todo
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Find and update Todo
for i, todo := range todos {
if todo.ID == id {
todos[i].Title = updateData.Title
todos[i].Completed = updateData.Completed
c.JSON(http.StatusOK, gin.H{
"message": "Todo updated successfully",
"data": todos[i],
})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Todo does not exist})
}
func deleteTodo(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
// Find and delete Todo
for i, todo := range todos {
if todo.ID == id {
todos = append(todos[:i], todos[i+1:]...)
c.JSON(http.StatusOK, gin.H{"message": "Todo deleted successfully"})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Todo does not exist"})
}
This example demonstrates a complete CRUD API. You can test it using Postman or curl:
# Create Todo
curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"Learn Gin Framework"}'
# Get all Todos
curl http://localhost:8080/api/todos
# Update Todo
curl -X PUT http://localhost:8080/api/todos/1 \
-H "Content-Type: application/json" \
-d '{"title":"In depth study of Gin framework","completed":true}'
# Delete Todo
curl -X DELETE http://localhost:8080/api/todos/1
Advanced Techniques
File upload
Gin also makes file uploads very simple:
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "File upload failed"})
return
}
// Save file
err = c.SaveUploadedFile(file, "./uploads/"+file.Filename)
if err != nil {
c.JSON(500, gin.H{"error": "File save failed"})
return
}
c.JSON(200, gin.H{"message": "File uploaded successfully"})
})
Static file service
// Service static file
r.Static("/static", "./static")
r.StaticFile("/favicon.ico", "./static/favicon.ico")
Template rendering
Although front-end and back-end separation is quite popular now, server-side rendering is still sometimes necessary.
r.LoadHTMLGlob("templates/*")
r.GET("/page", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "Gin Example Page",
"message": "Hello from Gin!",
})
})
Performance optimization suggestions
- Use middleware wisely : Avoid doing repetitive work in middleware that needs to be executed for every request.
- Avoid unnecessary JSON serialization : If you are only transmitting data, consider using JSON serialization
c.String()or JSON serialization.c.Data() - Use connection pooling : If you are connecting to a database, remember to configure connection pooling.
- Enable compression : For large response volumes, the gzip middleware can be used.
import "github.com/gin-contrib/gzip"
r.Use(gzip.Gzip(gzip.DefaultCompression))
Common problems and solutions
Question 1 : How to solve cross-domain issues?
Using CORS middleware:
import "github.com/gin-contrib/cors"
r.Use(cors.Default())
Question 2 : How to gracefully shut down a server?
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server startup failed: %s\n", err)
}
}()
// Waiting for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down the server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced shutdown:", err)
}
log.Println("The server has been shut down")
Summarize
The Gin framework is truly a powerful tool for Go web development! Here’s a summary of its advantages:
- Excellent performance : Based on httprouter, route lookup is extremely fast.
- Simple and easy to use : The API design is intuitive and has a low learning curve.
- Feature-rich : Includes middleware, parameter binding, route grouping, and more.
- Active community : comprehensive documentation, abundant third-party plugins
If you need to quickly develop high-performance Web APIs, Gin is definitely a good choice.