RESTful API 设计实战 #

本节将通过一个完整的用户管理API项目,演示如何使用Gin框架设计和实现符合RESTful规范的Web API。

项目概述 #

我们将构建一个用户管理API,包含以下功能:

  • 用户的增删改查(CRUD)
  • 分页和过滤查询
  • 统一的响应格式
  • 错误处理
  • 数据验证

项目结构 #

text
user-api/
├── main.go
├── config/
│   └── config.go
├── models/
│   └── user.go
├── handlers/
│   └── user_handler.go
├── middleware/
│   └── response.go
├── validators/
│   └── user_validator.go
├── services/
│   └── user_service.go
└── router/
    └── router.go

数据模型定义 #

go
// models/user.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    Username  string         `json:"username" gorm:"uniqueIndex;size:50;not null"`
    Email     string         `json:"email" gorm:"uniqueIndex;size:100;not null"`
    Password  string         `json:"-" gorm:"size:255;not null"`
    Nickname  string         `json:"nickname" gorm:"size:50"`
    Avatar    string         `json:"avatar" gorm:"size:255"`
    Status    int            `json:"status" gorm:"default:1"` // 1: active, 0: inactive
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

func (User) TableName() string {
    return "users"
}

type UserResponse struct {
    ID        uint      `json:"id"`
    Username  string    `json:"username"`
    Email     string    `json:"email"`
    Nickname  string    `json:"nickname"`
    Avatar    string    `json:"avatar"`
    Status    int       `json:"status"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

func (u *User) ToResponse() *UserResponse {
    return &UserResponse{
        ID:        u.ID,
        Username:  u.Username,
        Email:     u.Email,
        Nickname:  u.Nickname,
        Avatar:    u.Avatar,
        Status:    u.Status,
        CreatedAt: u.CreatedAt,
        UpdatedAt: u.UpdatedAt,
    }
}

统一响应格式 #

go
// middleware/response.go
package middleware

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
    Meta    *Meta       `json:"meta,omitempty"`
}

type Meta struct {
    Page      int   `json:"page"`
    PageSize  int   `json:"page_size"`
    Total     int64 `json:"total"`
    TotalPage int   `json:"total_page"`
}

func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: "success",
        Data:    data,
    })
}

func SuccessWithMeta(c *gin.Context, data interface{}, meta *Meta) {
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: "success",
        Data:    data,
        Meta:    meta,
    })
}

func Created(c *gin.Context, data interface{}) {
    c.JSON(http.StatusCreated, Response{
        Code:    0,
        Message: "created successfully",
        Data:    data,
    })
}

func NoContent(c *gin.Context) {
    c.Status(http.StatusNoContent)
}

func BadRequest(c *gin.Context, message string) {
    c.JSON(http.StatusBadRequest, Response{
        Code:    http.StatusBadRequest,
        Message: message,
    })
}

func NotFound(c *gin.Context, message string) {
    c.JSON(http.StatusNotFound, Response{
        Code:    http.StatusNotFound,
        Message: message,
    })
}

func InternalError(c *gin.Context, message string) {
    c.JSON(http.StatusInternalServerError, Response{
        Code:    http.StatusInternalServerError,
        Message: message,
    })
}

func ValidationError(c *gin.Context, errors interface{}) {
    c.JSON(http.StatusUnprocessableEntity, Response{
        Code:    http.StatusUnprocessableEntity,
        Message: "validation failed",
        Data:    errors,
    })
}

数据验证 #

go
// validators/user_validator.go
package validators

import (
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)

type CreateUserRequest struct {
    Username string `json:"username" binding:"required,min=3,max=50,alphanum"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6,max=20"`
    Nickname string `json:"nickname" binding:"max=50"`
}

type UpdateUserRequest struct {
    Nickname string `json:"nickname" binding:"max=50"`
    Avatar   string `json:"avatar" binding:"url"`
    Status   *int   `json:"status" binding:"omitempty,oneof=0 1"`
}

type ListUsersRequest struct {
    Page     int    `form:"page" binding:"min=1"`
    PageSize int    `form:"page_size" binding:"min=1,max=100"`
    Keyword  string `form:"keyword"`
    Status   *int   `form:"status" binding:"omitempty,oneof=0 1"`
}

func (r *ListUsersRequest) GetOffset() int {
    return (r.Page - 1) * r.PageSize
}

func (r *ListUsersRequest) GetLimit() int {
    return r.PageSize
}

type ChangePasswordRequest struct {
    OldPassword string `json:"old_password" binding:"required"`
    NewPassword string `json:"new_password" binding:"required,min=6,max=20"`
}

func GetValidationErrors(err error) map[string]string {
    errors := make(map[string]string)
    
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, fieldErr := range validationErrors {
            switch fieldErr.Tag() {
            case "required":
                errors[fieldErr.Field()] = "此字段为必填项"
            case "email":
                errors[fieldErr.Field()] = "请输入有效的邮箱地址"
            case "min":
                errors[fieldErr.Field()] = "长度不能小于" + fieldErr.Param()
            case "max":
                errors[fieldErr.Field()] = "长度不能超过" + fieldErr.Param()
            case "alphanum":
                errors[fieldErr.Field()] = "只能包含字母和数字"
            default:
                errors[fieldErr.Field()] = "验证失败"
            }
        }
    }
    
    return errors
}

func BindAndValidate(c *gin.Context, req interface{}) bool {
    if err := c.ShouldBindJSON(req); err != nil {
        return false
    }
    return true
}

业务逻辑层 #

go
// services/user_service.go
package services

import (
    "errors"
    "user-api/models"
    "user-api/validators"
    "gorm.io/gorm"
)

type UserService struct {
    db *gorm.DB
}

func NewUserService(db *gorm.DB) *UserService {
    return &UserService{db: db}
}

func (s *UserService) Create(req *validators.CreateUserRequest) (*models.User, error) {
    var count int64
    s.db.Model(&models.User{}).Where("username = ? OR email = ?", req.Username, req.Email).Count(&count)
    if count > 0 {
        return nil, errors.New("用户名或邮箱已存在")
    }
    
    user := &models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: req.Password,
        Nickname: req.Nickname,
        Status:   1,
    }
    
    if err := s.db.Create(user).Error; err != nil {
        return nil, err
    }
    
    return user, nil
}

func (s *UserService) GetByID(id uint) (*models.User, error) {
    var user models.User
    if err := s.db.First(&user, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("用户不存在")
        }
        return nil, err
    }
    return &user, nil
}

func (s *UserService) List(req *validators.ListUsersRequest) ([]models.User, int64, error) {
    var users []models.User
    var total int64
    
    query := s.db.Model(&models.User{})
    
    if req.Keyword != "" {
        query = query.Where("username LIKE ? OR nickname LIKE ?", 
            "%"+req.Keyword+"%", "%"+req.Keyword+"%")
    }
    
    if req.Status != nil {
        query = query.Where("status = ?", *req.Status)
    }
    
    query.Count(&total)
    
    if err := query.Offset(req.GetOffset()).Limit(req.GetLimit()).Find(&users).Error; err != nil {
        return nil, 0, err
    }
    
    return users, total, nil
}

func (s *UserService) Update(id uint, req *validators.UpdateUserRequest) (*models.User, error) {
    user, err := s.GetByID(id)
    if err != nil {
        return nil, err
    }
    
    updates := make(map[string]interface{})
    
    if req.Nickname != "" {
        updates["nickname"] = req.Nickname
    }
    if req.Avatar != "" {
        updates["avatar"] = req.Avatar
    }
    if req.Status != nil {
        updates["status"] = *req.Status
    }
    
    if err := s.db.Model(user).Updates(updates).Error; err != nil {
        return nil, err
    }
    
    return s.GetByID(id)
}

func (s *UserService) Delete(id uint) error {
    result := s.db.Delete(&models.User{}, id)
    if result.Error != nil {
        return result.Error
    }
    if result.RowsAffected == 0 {
        return errors.New("用户不存在")
    }
    return nil
}

处理器层 #

go
// handlers/user_handler.go
package handlers

import (
    "strconv"
    "user-api/middleware"
    "user-api/services"
    "user-api/validators"
    "github.com/gin-gonic/gin"
)

type UserHandler struct {
    service *services.UserService
}

func NewUserHandler(service *services.UserService) *UserHandler {
    return &UserHandler{service: service}
}

func (h *UserHandler) Create(c *gin.Context) {
    var req validators.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        middleware.ValidationError(c, validators.GetValidationErrors(err))
        return
    }
    
    user, err := h.service.Create(&req)
    if err != nil {
        middleware.BadRequest(c, err.Error())
        return
    }
    
    middleware.Created(c, user.ToResponse())
}

func (h *UserHandler) GetByID(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        middleware.BadRequest(c, "无效的用户ID")
        return
    }
    
    user, err := h.service.GetByID(uint(id))
    if err != nil {
        middleware.NotFound(c, err.Error())
        return
    }
    
    middleware.Success(c, user.ToResponse())
}

func (h *UserHandler) List(c *gin.Context) {
    var req validators.ListUsersRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        middleware.ValidationError(c, validators.GetValidationErrors(err))
        return
    }
    
    if req.Page == 0 {
        req.Page = 1
    }
    if req.PageSize == 0 {
        req.PageSize = 10
    }
    
    users, total, err := h.service.List(&req)
    if err != nil {
        middleware.InternalError(c, "获取用户列表失败")
        return
    }
    
    responses := make([]interface{}, len(users))
    for i, user := range users {
        responses[i] = user.ToResponse()
    }
    
    totalPage := int(total) / req.PageSize
    if int(total)%req.PageSize > 0 {
        totalPage++
    }
    
    middleware.SuccessWithMeta(c, responses, &middleware.Meta{
        Page:      req.Page,
        PageSize:  req.PageSize,
        Total:     total,
        TotalPage: totalPage,
    })
}

func (h *UserHandler) Update(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        middleware.BadRequest(c, "无效的用户ID")
        return
    }
    
    var req validators.UpdateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        middleware.ValidationError(c, validators.GetValidationErrors(err))
        return
    }
    
    user, err := h.service.Update(uint(id), &req)
    if err != nil {
        middleware.BadRequest(c, err.Error())
        return
    }
    
    middleware.Success(c, user.ToResponse())
}

func (h *UserHandler) Delete(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        middleware.BadRequest(c, "无效的用户ID")
        return
    }
    
    if err := h.service.Delete(uint(id)); err != nil {
        middleware.NotFound(c, err.Error())
        return
    }
    
    middleware.NoContent(c)
}

路由配置 #

go
// router/router.go
package router

import (
    "user-api/handlers"
    "user-api/services"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func Setup(db *gorm.DB) *gin.Engine {
    r := gin.Default()
    
    userService := services.NewUserService(db)
    userHandler := handlers.NewUserHandler(userService)
    
    api := r.Group("/api/v1")
    {
        users := api.Group("/users")
        {
            users.POST("", userHandler.Create)
            users.GET("", userHandler.List)
            users.GET("/:id", userHandler.GetByID)
            users.PUT("/:id", userHandler.Update)
            users.DELETE("/:id", userHandler.Delete)
        }
    }
    
    return r
}

主程序入口 #

go
// main.go
package main

import (
    "log"
    "user-api/models"
    "user-api/router"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
    dsn := "user:password@tcp(127.0.0.1:3306)/userdb?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }
    
    db.AutoMigrate(&models.User{})
    
    r := router.Setup(db)
    
    log.Println("服务器启动在 :8080")
    if err := r.Run(":8080"); err != nil {
        log.Fatal("服务器启动失败:", err)
    }
}

API 测试示例 #

创建用户 #

bash
curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "johndoe",
    "email": "john@example.com",
    "password": "password123",
    "nickname": "John"
  }'

响应:

json
{
  "code": 0,
  "message": "created successfully",
  "data": {
    "id": 1,
    "username": "johndoe",
    "email": "john@example.com",
    "nickname": "John",
    "status": 1,
    "created_at": "2026-03-28T10:00:00Z",
    "updated_at": "2026-03-28T10:00:00Z"
  }
}

获取用户列表 #

bash
curl "http://localhost:8080/api/v1/users?page=1&page_size=10&keyword=john"

响应:

json
{
  "code": 0,
  "message": "success",
  "data": [...],
  "meta": {
    "page": 1,
    "page_size": 10,
    "total": 1,
    "total_page": 1
  }
}

获取单个用户 #

bash
curl http://localhost:8080/api/v1/users/1

更新用户 #

bash
curl -X PUT http://localhost:8080/api/v1/users/1 \
  -H "Content-Type: application/json" \
  -d '{
    "nickname": "John Doe",
    "avatar": "https://example.com/avatar.jpg"
  }'

删除用户 #

bash
curl -X DELETE http://localhost:8080/api/v1/users/1

RESTful 设计原则 #

HTTP 方法对应操作 #

HTTP方法 操作 示例
GET 获取资源 GET /users
POST 创建资源 POST /users
PUT 更新资源(完整) PUT /users/1
PATCH 更新资源(部分) PATCH /users/1
DELETE 删除资源 DELETE /users/1

HTTP 状态码规范 #

状态码 含义 使用场景
200 OK 成功获取或更新资源
201 Created 成功创建资源
204 No Content 成功删除资源
400 Bad Request 请求参数错误
401 Unauthorized 未认证
403 Forbidden 无权限
404 Not Found 资源不存在
422 Unprocessable Entity 验证失败
500 Internal Server Error 服务器错误

最佳实践 #

1. 版本控制 #

go
api := r.Group("/api/v1")
{
    // v1 版本的路由
}

apiV2 := r.Group("/api/v2")
{
    // v2 版本的路由
}

2. 分页参数标准化 #

go
type Pagination struct {
    Page     int `form:"page" binding:"min=1"`
    PageSize int `form:"page_size" binding:"min=1,max=100"`
}

func (p *Pagination) GetOffset() int {
    return (p.Page - 1) * p.PageSize
}

func (p *Pagination) GetLimit() int {
    return p.PageSize
}

3. 过滤和排序 #

go
type ListRequest struct {
    Pagination
    Keyword  string `form:"keyword"`
    Status   *int   `form:"status"`
    SortBy   string `form:"sort_by"`
    SortDesc bool   `form:"sort_desc"`
}

4. 嵌套资源 #

go
users := api.Group("/users")
{
    users.GET("/:id/posts", userHandler.GetPosts)
    users.POST("/:id/posts", userHandler.CreatePost)
}

posts := api.Group("/posts")
{
    posts.GET("/:id/comments", postHandler.GetComments)
}

小结 #

本节通过一个完整的用户管理API项目,演示了:

  1. 项目结构设计:分层架构,职责清晰
  2. 统一响应格式:标准化API响应
  3. 数据验证:请求参数校验
  4. RESTful规范:遵循HTTP语义
  5. 错误处理:友好的错误信息

这个项目可以作为其他API项目的基础模板,根据实际需求进行扩展。

最后更新:2026-03-28