进阶3-4 小时·Web 服务

Go REST API 服务

使用 Gin + GORM 构建一个带 JWT 认证、分页、Swagger 文档的 RESTful API

gogingormjwtswagger

实战案例:使用 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

最佳实践

  1. 配置管理:不要将敏感信息(如数据库密码、JWT密钥)硬编码。使用环境变量或配置文件(如 Viper 库)进行管理。
  2. 日志记录:使用结构化日志库(如 zerologzap)替代标准 log 包,便于日志分析和监控。
  3. 错误处理:在控制器中统一格式化错误响应。为不同类型的错误定义不同的 HTTP 状态码和错误码。
  4. 数据库事务:对于需要多个操作保持数据一致性的业务逻辑(如注册用户并初始化资料),应在服务层使用数据库事务。
  5. 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 性能更好。

扩展挑战

  1. 添加角色与权限控制 (RBAC):扩展用户模型,增加 Role 字段(如 user, admin),并创建一个更精细的中间件来检查用户是否有权执行某个操作。
  2. 实现缓存层:使用 Redis 缓存频繁访问的用户资料或列表数据,减少数据库压力。
  3. 编写单元测试与集成测试:为你的服务层和控制器编写测试。使用 go-sqlmock 或真实的测试数据库来测试 GORM 操作。
  4. 部署与容器化:编写 Dockerfiledocker-compose.yml 文件,将你的应用和数据库打包成容器,实现一键部署。

相关课程

课程相关章节
Go 语言基础结构体
Go 语言基础方法与接收者
Go 语言基础接口
Go 语言基础错误处理
Go 语言基础项目结构与最佳实践