实战案例:使用 Gin + GORM 构建企业级 RESTful API 服务
场景描述
本案例将指导你构建一个生产就绪的用户管理后端服务,它提供完整的 RESTful API、安全的 JWT 认证、高效的数据库交互以及自动化 API 文档。你将从零开始,打造一个结构清晰、易于维护和扩展的 Go Web 应用。
你将学到
- 框架集成:使用 Gin 框架搭建高性能的 HTTP 路由与处理器。
- ORM 实践:利用 GORM 进行数据库模型定义、关联查询与分页处理。
- 安全认证:实现基于 JWT (JSON Web Token) 的无状态认证与授权中间件。
- API 文档:通过 Swagger 工具自动生成交互式 API 文档。
- 工程化实践:遵循 Go 社区推荐的项目结构,组织路由、控制器、服务与数据层。
前置知识
在开始本案例前,请确保你已经掌握以下基础知识:
- Go 语言基础:包括变量、函数、结构体、方法、接口和错误处理。
- Go 项目结构与最佳实践:理解
cmd,internal,pkg等常见目录布局。 - 基本的 HTTP 协议和 RESTful API 设计概念。
- 关系型数据库(如 MySQL, PostgreSQL)的基本 SQL 知识。
架构设计
我们将采用一个清晰的分层架构,将职责分离,便于测试和维护。
[客户端]
|
v
[路由层] <-- 使用 Gin Router 定义 API 路由
|
v
[中间件层] <-- JWT 认证、日志记录、CORS 等
|
v
[控制器层] <-- 处理 HTTP 请求,解析参数,调用服务层
|
v
[服务层] <-- 包含核心业务逻辑,协调多个数据模型
|
v
[数据访问层] <-- 使用 GORM 与数据库交互
|
v
[数据库]
实现步骤
第 1 步:项目初始化与依赖安装
创建一个新的 Go 模块并安装所需依赖。
# 创建项目目录
mkdir go-gin-jwt-api && cd go-gin-jwt-api
# 初始化 Go 模块
go mod init github.com/yourusername/go-gin-jwt-api
# 安装核心依赖
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/golang-jwt/jwt/v5
go get -u github.com/swaggo/swag/cmd/swag
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files
第 2 步:定义数据模型 (Model)
我们定义一个 User 模型,并使用 GORM 的标签来控制数据库映射。
// internal/model/user.go
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除
Username string `json:"username" gorm:"uniqueIndex;size:64;not null"`
Password string `json:"-" gorm:"not null"` // 密码在JSON中不显示
Email string `json:"email" gorm:"uniqueIndex;size:128;not null"`
}
第 3 步:创建 JWT 工具函数
编写生成和解析 JWT 令牌的函数。在生产环境中,请将密钥 (secret) 保存在环境变量或配置中心。
// pkg/utils/jwt.go
package utils
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-strong-jwt-secret-key")
// JWTClaims 定义JWT载荷
type JWTClaims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken 为用户生成JWT令牌
func GenerateToken(userID uint, username string) (string, error) {
claims := JWTClaims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// ParseToken 解析JWT令牌
func ParseToken(tokenString string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrSignatureInvalid
}
第 4 步:实现 JWT 认证中间件
这个中间件将拦截受保护的路由,并验证请求头中的 Authorization: Bearer <token>。
// internal/middleware/jwt_auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go-gin-jwt-api/pkg/utils"
)
// JWTAuthMiddleware 返回一个Gin中间件函数
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
c.Abort()
return
}
// 按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header must be Bearer {token}"})
c.Abort()
return
}
claims, err := utils.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
// 将用户信息存入上下文,供后续处理器使用
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
第 5 步:定义请求/响应结构体 (DTO)
为了规范 API 的输入输出,我们定义专用的 DTO (Data Transfer Object) 结构体。
// internal/dto/user_dto.go
package dto
import "go-gin-jwt-api/internal/model"
// CreateUserRequest 创建用户的请求体
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=64"`
Password string `json:"password" binding:"required,min=8,max=128"`
Email string `json:"email" binding:"required,email"`
}
// LoginRequest 登录请求体
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录成功响应体
type LoginResponse struct {
Token string `json:"token"`
User model.User `json:"user"`
}
// PaginationRequest 分页请求参数
type PaginationRequest struct {
Page int `form:"page" binding:"omitempty,min=1"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"`
}
第 6 步:实现数据库连接与初始化 (Repository)
使用 GORM 初始化数据库连接,并实现 UserRepository。
// internal/repository/user_repo.go
package repository
import (
"go-gin-jwt-api/internal/model"
"gorm.io/gorm"
)
type UserRepository struct {
DB *gorm.DB
}
// NewUserRepository 创建UserRepository实例
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{DB: db}
}
// Create 创建用户
func (r *UserRepository) Create(user *model.User) error {
return r.DB.Create(user).Error
}
// FindByUsername 根据用户名查找用户
func (r *UserRepository) FindByUsername(username string) (*model.User, error) {
var user model.User
err := r.DB.Where("username = ?", username).First(&user).Error
return &user, err
}
// FindByID 根据ID查找用户
func (r *UserRepository) FindByID(id uint) (*model.User, error) {
var user model.User
err := r.DB.First(&user, id).Error
return &user, err
}
// List 分页获取用户列表
func (r *UserRepository) List(page, pageSize int) ([]model.User, int64, error) {
var users []model.User
var total int64
// 计算总记录数
r.DB.Model(&model.User{}).Count(&total)
// 分页查询
offset := (page - 1) * pageSize
err := r.DB.Offset(offset).Limit(pageSize).Find(&users).Error
return users, total, err
}
第 7 步:编写服务层 (Service)
服务层封装业务逻辑,处理如密码加密等操作。
// internal/service/user_service.go
package service
import (
"go-gin-jwt-api/internal/model"
"go-gin-jwt-api/internal/repository"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
UserRepo *repository.UserRepository
}
func NewUserService(userRepo *repository.UserRepository) *UserService {
return &UserService{UserRepo: userRepo}
}
// RegisterUser 注册新用户
func (s *UserService) RegisterUser(username, password, email string) (*model.User, error) {
// 对密码进行哈希处理
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &model.User{
Username: username,
Password: string(hashedPassword),
Email: email,
}
err = s.UserRepo.Create(user)
return user, err
}
// AuthenticateUser 验证用户登录
func (s *UserService) AuthenticateUser(username, password string) (*model.User, error) {
user, err := s.UserRepo.FindByUsername(username)
if err != nil {
return nil, err
}
// 比较密码哈希值
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
return nil, err
}
return user, nil
}
// GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(id uint) (*model.User, error) {
return s.UserRepo.FindByID(id)
}
// GetUserList 获取用户列表(分页)
func (s *UserService) GetUserList(page, pageSize int) ([]model.User, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20 // 设置默认分页大小
}
return s.UserRepo.List(page, pageSize)
}
第 8 步:实现控制器 (Controller)
控制器负责解析 HTTP 请求,并调用相应的服务方法。
// internal/controller/user_controller.go
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"go-gin-jwt-api/internal/dto"
"go-gin-jwt-api/internal/service"
"go-gin-jwt-api/pkg/utils"
)
type UserController struct {
UserService *service.UserService
}
func NewUserController(userService *service.UserService) *UserController {
return &UserController{UserService: userService}
}
// Register 用户注册
// @Summary 用户注册
// @Description 创建一个新用户
// @Tags 用户
// @Accept json
// @Produce json
// @Param input body dto.CreateUserRequest true "用户信息"
// @Success 201 {object} model.User
// @Router /api/v1/users/register [post]
func (ctrl *UserController) Register(c *gin.Context) {
var req dto.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := ctrl.UserService.RegisterUser(req.Username, req.Password, req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
c.JSON(http.StatusCreated, user)
}
// Login 用户登录
// @Summary 用户登录
// @Description 用户登录并获取JWT令牌
// @Tags 认证
// @Accept json
// @Produce json
// @Param input body dto.LoginRequest true "登录信息"
// @Success 200 {object} dto.LoginResponse
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/login [post]
func (ctrl *UserController) Login(c *gin.Context) {
var req dto.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := ctrl.UserService.AuthenticateUser(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
token, err := utils.GenerateToken(user.ID, user.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(http.StatusOK, dto.LoginResponse{
Token: token,
User: *user,
})
}
// GetProfile 获取当前登录用户的资料
// @Summary 获取个人资料
// @Description 获取当前登录用户的详细信息(需要JWT认证)
// @Tags 用户
// @Security BearerAuth
// @Produce json
// @Success 200 {object} model.User
// @Failure 401 {object} map[string]string
// @Router /api/v1/users/profile [get]
func (ctrl *UserController) GetProfile(c *gin.Context) {
// 从JWT中间件设置的上下文中获取用户ID
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found in context"})
return
}
user, err := ctrl.UserService.GetUserByID(userID.(uint))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
}
// ListUsers 获取用户列表(需要认证)
// @Summary 获取用户列表
// @Description 分页获取所有用户(需要JWT认证)
// @Tags 用户
// @Security BearerAuth
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/users [get]
func (ctrl *UserController) ListUsers(c *gin.Context) {
var req dto.PaginationRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
users, total, err := ctrl.UserService.GetUserList(req.Page, req.PageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": total,
"page": req.Page,
"page_size": req.PageSize,
})
}
第 9 步:设置路由与启动服务器
现在,我们将所有组件组装起来,创建一个完整的 API 服务器。
// cmd/server/main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go-gin-jwt-api/internal/controller"
"go-gin-jwt-api/internal/middleware"
"go-gin-jwt-api/internal/model"
"go-gin-jwt-api/internal/repository"
"go-gin-jwt-api/internal/service"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// @title 用户管理 API
// @version 1.0
// @description 这是一个使用 Gin + GORM + JWT 构建的用户管理API示例
// @host localhost:8080
// @BasePath /
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
// 1. 连接数据库(请替换为你的数据库连接字符串)
dsn := "root:password@tcp(127.0.0.1:3306)/go_jwt_api?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// 2. 自动迁移数据库表
err = db.AutoMigrate(&model.User{})
if err != nil {
log.Fatal("Failed to migrate database:", err)
}
// 3. 初始化依赖(仓库 -> 服务 -> 控制器)
userRepo := repository.NewUserRepository(db)
userService := service.NewUserService(userRepo)
userController := controller.NewUserController(userService)
// 4. 初始化Gin路由器
r := gin.Default()
// 5. 添加Swagger文档路由
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 6. 定义API路由组
api := r.Group("/api/v1")
{
// 公开路由(无需认证)
api.POST("/auth/login", userController.Login)
api.POST("/users/register", userController.Register)
// 需要认证的路由
protected := api.Group("/users")
protected.Use(middleware.JWTAuthMiddleware())
{
protected.GET("/profile", userController.GetProfile)
protected.GET("", userController.ListUsers)
}
}
// 7. 启动服务器
log.Println("Server starting on :8080")
log.Println("Swagger UI available at: http://localhost:8080/swagger/index.html")
if err := r.Run(":8080"); err != nil {
log.Fatal("Failed to start server:", err)
}
}
第 10 步:生成并使用 Swagger 文档
安装 Swagger CLI 并生成文档。
# 安装 swag CLI
go install github.com/swaggo/swag/cmd/swag@latest
# 在项目根目录生成文档
swag init
# 生成的文档会保存在 ./docs 目录下
# 现在可以访问 http://localhost:8080/swagger/index.html 查看交互式文档
完整项目结构
go-gin-jwt-api/
├── cmd/
│ └── server/
│ └── main.go # 应用入口点
├── internal/ # 私有应用代码
│ ├── controller/ # HTTP 处理器
│ │ └── user_controller.go
│ ├── dto/ # 数据传输对象
│ │ └── user_dto.go
│ ├── middleware/ # Gin 中间件
│ │ └── jwt_auth.go
│ ├── model/ # 数据库模型
│ │ └── user.go
│ ├── repository/ # 数据访问层
│ │ └── user_repo.go
│ └── service/ # 业务逻辑层
│ └── user_service.go
├── pkg/ # 可供外部使用的包
│ └── utils/
│ └── jwt.go
├── docs/ # Swagger 自动生成的文档
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── go.mod
├── go.sum
└── README.md
最佳实践
- 配置管理:不要将敏感信息(如数据库密码、JWT密钥)硬编码。使用环境变量或配置文件(如 Viper 库)进行管理。
- 日志记录:使用结构化日志库(如
zerolog或zap)替代标准log包,便于日志分析和监控。 - 错误处理:在控制器中统一格式化错误响应。为不同类型的错误定义不同的 HTTP 状态码和错误码。
- 数据库事务:对于需要多个操作保持数据一致性的业务逻辑(如注册用户并初始化资料),应在服务层使用数据库事务。
- API 版本控制:如示例所示,使用 URL 路径 (
/api/v1/...) 进行版本控制,为未来兼容性提供保障。
常见问题
Q1: 如何实现 JWT 令牌刷新(Refresh Token)机制?
A: 可以生成两个令牌:一个短期有效的 Access Token(如15分钟)和一个长期有效的 Refresh Token(如7天)。Access Token 用于访问API,Refresh Token 用于在 Access Token 过期后获取新的 Access Token。你需要在数据库中存储 Refresh Token 以支持吊销。
Q2: 数据库连接失败如何处理?
A: 在 main.go 中,连接数据库失败时直接终止程序是合理的,因为服务无法在没有数据库的情况下启动。在生产环境中,你还可以实现健康检查接口 (/health) 来监控数据库连接状态。
Q3: 分页查询如何优化大数据量下的性能?
A: 避免使用 COUNT(*) 在大数据表上,这可能会很慢。考虑使用估算值或缓存总数。使用基于游标的分页(使用上一页最后一条记录的ID)比使用 OFFSET 性能更好。
扩展挑战
- 添加角色与权限控制 (RBAC):扩展用户模型,增加
Role字段(如user,admin),并创建一个更精细的中间件来检查用户是否有权执行某个操作。 - 实现缓存层:使用 Redis 缓存频繁访问的用户资料或列表数据,减少数据库压力。
- 编写单元测试与集成测试:为你的服务层和控制器编写测试。使用
go-sqlmock或真实的测试数据库来测试 GORM 操作。 - 部署与容器化:编写
Dockerfile和docker-compose.yml文件,将你的应用和数据库打包成容器,实现一键部署。
相关课程
| 课程 | 相关章节 |
|---|---|
| Go 语言基础 | 结构体 |
| Go 语言基础 | 方法与接收者 |
| Go 语言基础 | 接口 |
| Go 语言基础 | 错误处理 |
| Go 语言基础 | 项目结构与最佳实践 |