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项目,演示了:
- 项目结构设计:分层架构,职责清晰
- 统一响应格式:标准化API响应
- 数据验证:请求参数校验
- RESTful规范:遵循HTTP语义
- 错误处理:友好的错误信息
这个项目可以作为其他API项目的基础模板,根据实际需求进行扩展。
最后更新:2026-03-28