Gin

技术栈
后端框架
gingolangweb框架REST API高性能中间件

概览

Gin 技术栈概览

Gin 是 Go 语言中最流行的 高性能 HTTP Web 框架,由 @gin-gonic 团队维护。它基于 httprouter 实现极致路由性能(比标准库快 40 倍),提供 Martini 风格的 API,是 Go 后端开发的事实标准。

解决什么问题

  • 高性能 API:路由基于 radix tree,零内存分配,QPS 远超 Express/Django
  • Go 后端快速上手:类似 Flask 的简洁 API,Go 初学者友好
  • 中间件生态:日志、恢复、CORS、限流、JWT 认证开箱即用
  • 毕设/微服务:编译为单一 5MB 二进制,部署极简

关键特性

  • 极致路由性能(httprouter radix tree)
  • 请求参数绑定与验证(binding tags)
  • 丰富的内置中间件(Logger、Recovery)
  • 支持 JSON/XML/YAML/Protobuf 渲染
  • 庞大的第三方中间件生态

安装

环境准备

  • 操作系统:Windows 10+ / macOS 12+ / Ubuntu 20.04+
  • 运行时版本:Go 1.18+(推荐 1.22+)
  • 依赖项:git(go mod 拉取依赖需要)

安装命令

Gin 是 Go 模块,通过 go get 安装:

# 1. 创建项目并初始化 Go Module
mkdir my-gin-app &;& cd my-gin-app
go mod init my-gin-app

# 2. 安装 Gin
go get -u github.com/gin-gonic/gin

# 3. 创建 main.go(见例程),然后运行
go run main.go

# 4. 编译为可执行文件
go build -o server.exe   # Windows
go build -o server       # macOS/Linux
./server

国内加速(如果 go get 失败)

go env -w GOPROXY=https://goproxy.cn,direct
go get -u github.com/gin-gonic/gin

可选常用中间件

go get github.com/gin-contrib/cors
go get github.com/golang-jwt/jwt/v5
go get github.com/swaggo/gin-swagger

常见安装问题

问题 解决方案
go get 卡住/超时 设置国内代理:go env -w GOPROXY=https://goproxy.cn,direct
编译报 undefined: gin 确认 go.mod 中有 require github.com/gin-gonic/gin,执行 go mod tidy
CGO 编译错误 纯 Gin 无需 CGO,若报 CGO 相关问题可 CGO_ENABLED=0 go build
端口 8080 被占用 Gin 默认不指定端口,在代码 r.Run(":3000") 中修改

示例

Gin + GORM 分页查询 — 通用分页封装

目标

实现一个可复用的 GORM 分页查询封装,支持排序、搜索、自定义条件,是毕设列表接口的标配。

完整代码

package main

import (
	"fmt"
	"math"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// ───── 数据模型 ─────
type Product struct {
	ID          uint    `gorm:"primaryKey" json:"id"`
	Name        string  `gorm:"size:100;index" json:"name"`
	Category    string  `gorm:"size:50;index" json:"category"`
	Price       float64 `json:"price"`
	Stock       int     `json:"stock"`
	Description string  `gorm:"type:text" json:"description"`
}

// ───── 通用分页结构 ─────
type Pagination struct {
	Page       int         `json:"page"`
	Limit      int         `json:"limit"`
	Total      int64       `json:"total"`
	TotalPages int         `json:"totalPages"`
	Data       interface{} `json:"data"`
}

type PageQuery struct {
	Page    int    `form:"page" binding:"omitempty,min=1"`
	Limit   int    `form:"limit" binding:"omitempty,min=1,max=100"`
	Sort    string `form:"sort"`    // 排序字段
	Order   string `form:"order"`   // asc/desc
	Keyword string `form:"keyword"` // 搜索关键词
}

// Paginate 通用分页查询函数
func Paginate(db *gorm.DB, query PageQuery, dest interface{}, searchFields ...string) (*Pagination, error) {
	// 默认值
	if query.Page == 0 {
		query.Page = 1
	}
	if query.Limit == 0 {
		query.Limit = 10
	}
	if query.Order == "" {
		query.Order = "desc"
	}

	q := db.Model(dest)

	// 关键词搜索(多字段 LIKE)
	if query.Keyword != "" {
		keyword := "%" + query.Keyword + "%"
		conditions := make([]string, len(searchFields))
		args := make([]interface{}, len(searchFields))
		for i, field := range searchFields {
			conditions[i] = fmt.Sprintf("%s LIKE ?", field)
			args[i] = keyword
		}
		if len(conditions) > 0 {
			q = q.Where(conditions[0], args[0])
			for i := 1; i < len(conditions); i++ {
				q = q.Or(conditions[i], args[i])
			}
		}
	}

	// 统计总数
	var total int64
	q.Count(&total)

	// 排序
	if query.Sort != "" {
		q = q.Order(fmt.Sprintf("%s %s", query.Sort, query.Order))
	} else {
		q = q.Order("id desc")
	}

	// 分页
	offset := (query.Page - 1) * query.Limit
	q.Limit(query.Limit).Offset(offset).Find(dest)

	return &Pagination{
		Page:       query.Page,
		Limit:      query.Limit,
		Total:      total,
		TotalPages: int(math.Ceil(float64(total) / float64(query.Limit))),
		Data:       dest,
	}, nil
}

// ───── 数据库初始化 ─────
var db *gorm.DB

func initDB() {
	var err error
	db, err = gorm.Open(sqlite.Open("shop.db"), &gorm.Config{})
	if err != nil {
		panic("数据库连接失败: " + err.Error())
	}
	db.AutoMigrate(&Product{})

	// 种子数据
	var count int64
	db.Model(&Product{}).Count(&count)
	if count == 0 {
		categories := []string{"电子产品", "图书", "文具", "运动器材", "生活用品"}
		for i := 1; i <= 50; i++ {
			db.Create(&Product{
				Name:        fmt.Sprintf("商品-%03d", i),
				Category:    categories[i%len(categories)],
				Price:       float64(i*10 + 5),
				Stock:       i * 3,
				Description: fmt.Sprintf("这是第 %d 个商品的描述", i),
			})
		}
		fmt.Println("✅ 已插入 50 条种子数据")
	}
}

// ───── Handler ─────
func listProducts(c *gin.Context) {
	var query PageQuery
	if err := c.ShouldBindQuery(&query); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var products []Product
	result, err := Paginate(db, query, &products, "name", "category", "description")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, result)
}

func main() {
	initDB()
	r := gin.Default()
	r.GET("/api/products", listProducts)
	r.Run(":8080")
}

测试

# 第1页,每页10条
curl "http://localhost:8080/api/products?page=1&limit=10"

# 搜索
curl "http://localhost:8080/api/products?keyword=电子产品"

# 按价格降序
curl "http://localhost:8080/api/products?sort=price&order=desc"

# 组合:搜索 + 分页 + 排序
curl "http://localhost:8080/api/products?keyword=商品&page=2&limit=5&sort=stock&order=asc"

响应示例

{
  "page": 1,
  "limit": 10,
  "total": 50,
  "totalPages": 5,
  "data": [
    {"id": 50, "name": "商品-050", "category": "生活用品", "price": 505, "stock": 150},
    ...
  ]
}

关键要点

  • Paginate 函数可复用到任何模型
  • searchFields 参数化搜索字段,防 SQL 注入
  • 种子数据方便答辩时直接演示分页效果

Gin RESTful API — 任务管理 Todo

目标

使用 Gin 框架快速搭建 Todo 任务 CRUD API,演示路由分组、参数绑定、中间件。

完整代码

package main

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

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

// Todo 模型
type Todo struct {
	ID        int       `json:"id"`
	Title     string    `json:"title" binding:"required"`
	Completed bool      `json:"completed"`
	CreatedAt time.Time `json:"createdAt"`
}

var (
	todos   = []Todo{}
	nextID  = 1
	mu      sync.Mutex
)

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

	// 分组路由
	v1 := r.Group("/api/v1")
	{
		v1.GET("/todos", getTodos)
		v1.GET("/todos/:id", getTodo)
		v1.POST("/todos", createTodo)
		v1.PUT("/todos/:id", updateTodo)
		v1.DELETE("/todos/:id", deleteTodo)
	}

	r.Run(":8080")
}

func getTodos(c *gin.Context) {
	mu.Lock()
	defer mu.Unlock()
	c.JSON(http.StatusOK, gin.H{"count": len(todos), "data": todos})
}

func getTodo(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	mu.Lock()
	defer mu.Unlock()
	for _, t := range todos {
		if t.ID == id {
			c.JSON(http.StatusOK, t)
			return
		}
	}
	c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
}

func createTodo(c *gin.Context) {
	var todo Todo
	if err := c.ShouldBindJSON(&todo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	mu.Lock()
	todo.ID = nextID
	nextID++
	todo.CreatedAt = time.Now()
	todos = append(todos, todo)
	mu.Unlock()
	c.JSON(http.StatusCreated, todo)
}

func updateTodo(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	var input Todo
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	mu.Lock()
	defer mu.Unlock()
	for i, t := range todos {
		if t.ID == id {
			todos[i].Title = input.Title
			todos[i].Completed = input.Completed
			c.JSON(http.StatusOK, todos[i])
			return
		}
	}
	c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
}

func deleteTodo(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	mu.Lock()
	defer mu.Unlock()
	for i, t := range todos {
		if t.ID == id {
			todos = append(todos[:i], todos[i+1:]...)
			c.JSON(http.StatusOK, gin.H{"message": "已删除"})
			return
		}
	}
	c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
}

运行步骤

go mod init todo-gin
go get github.com/gin-gonic/gin
go run main.go

测试

# 新增任务
curl -X POST http://localhost:8080/api/v1/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"完成毕设论文"}'

# 获取全部
curl http://localhost:8080/api/v1/todos

# 更新为已完成
curl -X PUT http://localhost:8080/api/v1/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"完成毕设论文","completed":true}'

# 删除
curl -X DELETE http://localhost:8080/api/v1/todos/1

预期输出

  • Gin 启动打印路由表,每个请求自动输出彩色日志
  • JSON 绑定 binding:"required" 自动校验,缺失 title 返回 400
  • 并发安全:sync.Mutex 保护共享切片

教程

Gin 毕设实战 — Go Web 开发从入门到上线

前言

Gin 是国内 Go 后端开发的事实标准。它性能极高、API 简洁,非常适合毕设中需要高性能接口的场景(如数据处理、实时计算)。

第一章:Gin 的 radix tree 路由

Gin 基于 httprouter 的压缩前缀树(radix tree),路由时间复杂度 O(log n):

GET /api/users
GET /api/users/:id
GET /api/users/:id/posts
POST /api/users
POST /api/posts

这些路由被组织成一棵树,查找效率极高。相比之下 Express 对每个路由遍历正则匹配。

第二章:请求验证

Gin 使用 validator 库做 struct tag 验证:

type CreateOrderReq struct {
    UserID    int     `json:"userId" binding:"required,min=1"`
    ProductID int     `json:"productId" binding:"required"`
    Quantity  int     `json:"quantity" binding:"required,min=1,max=100"`
    Coupon    string  `json:"coupon" binding:"omitempty,len=6"`
    Email     string  `json:"email" binding:"required,email"`
}

func createOrder(c *gin.Context) {
    var req CreateOrderReq
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // 验证通过,req 可直接使用
}

常用验证标签:requiredminmaxlenemailurloneofgtelte

第三章:中间件开发

// 自定义 JWT 中间件
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" || !strings.HasPrefix(token, "Bearer ") {
            c.AbortWithStatusJSON(401, gin.H{"error": "未登录"})
            return
        }
        claims, err := parseToken(token[7:])
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "Token 无效"})
            return
        }
        c.Set("userId", claims.UserID) // 存入上下文
        c.Next()
    }
}

// 使用
r.Group("/api").Use(AuthMiddleware())
{
    // 此组内所有路由都需认证
}

第四章:使用 GORM 操作数据库

// 模型定义
type Article struct {
    ID        uint   `gorm:"primaryKey"`
    Title     string `gorm:"size:200;not null"`
    Content   string `gorm:"type:text"`
    UserID    uint
    CreatedAt time.Time
}

// 初始化
db, _ := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
db.AutoMigrate(&Article{})

// 在 Handler 中使用
func listArticles(c *gin.Context) {
    var articles []Article
    db.Find(&articles)
    c.JSON(200, articles)
}

第五章:优雅关停

srv := &http.Server{Addr: ":8080", Handler: r}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx) // 等待进行中的请求完成

思考题

  1. Gin 的 ShouldBindJSONMustBindJSON 区别是什么?
  2. 如何在 Gin 中实现请求限流(rate limiting)?
  3. Go 的 context.Context 在 Web 请求中有什么作用?