3.5.1 RESTful 设计原则 #
REST(Representational State Transfer)是一种软件架构风格,由 Roy Fielding 在 2000 年的博士论文中提出。RESTful API 已成为现代 Web 服务设计的标准,本节将深入讲解 REST 的核心原则和最佳实践。
REST 架构约束 #
REST 架构风格定义了六个基本约束条件,这些约束共同定义了 RESTful 系统的特征。
1. 客户端-服务器架构(Client-Server) #
客户端和服务器之间必须分离,各自独立演化。这种分离提高了用户界面的可移植性,简化了服务器组件。
// 服务器端:提供 API 接口
func main() {
r := gin.Default()
// API 路由组
api := r.Group("/api/v1")
{
api.GET("/users", getUsers)
api.POST("/users", createUser)
api.GET("/users/:id", getUser)
api.PUT("/users/:id", updateUser)
api.DELETE("/users/:id", deleteUser)
}
r.Run(":8080")
}
2. 无状态(Stateless) #
服务器不应该保存客户端的状态信息。每个请求都必须包含处理该请求所需的所有信息。
// 错误示例:服务器保存会话状态
var sessions = make(map[string]*UserSession)
func loginHandler(c *gin.Context) {
// 不推荐:在服务器端保存会话
sessionID := generateSessionID()
sessions[sessionID] = &UserSession{
UserID: userID,
LoginTime: time.Now(),
}
}
// 正确示例:使用 JWT 令牌
func loginHandler(c *gin.Context) {
var loginReq LoginRequest
if err := c.ShouldBindJSON(&loginReq); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 验证用户凭据
user, err := authenticateUser(loginReq.Username, loginReq.Password)
if err != nil {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
// 生成 JWT 令牌(无状态)
token, err := generateJWT(user.ID)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(200, gin.H{
"token": token,
"user": user,
})
}
3. 可缓存(Cacheable) #
响应数据必须明确标记为可缓存或不可缓存,以提高网络效率。
func getUserHandler(c *gin.Context) {
userID := c.Param("id")
user, err := getUserByID(userID)
if err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
// 设置缓存头
c.Header("Cache-Control", "public, max-age=300") // 5分钟缓存
c.Header("ETag", fmt.Sprintf(`"%d"`, user.UpdatedAt.Unix()))
// 检查 If-None-Match 头
if match := c.GetHeader("If-None-Match"); match != "" {
if match == fmt.Sprintf(`"%d"`, user.UpdatedAt.Unix()) {
c.Status(304) // Not Modified
return
}
}
c.JSON(200, user)
}
4. 统一接口(Uniform Interface) #
REST 的核心特征是统一接口,包含四个约束:
资源标识(Resource Identification) #
每个资源都有唯一的标识符(URI)。
// 良好的资源标识设计
// GET /api/v1/users - 获取用户列表
// GET /api/v1/users/123 - 获取特定用户
// GET /api/v1/users/123/posts - 获取用户的文章
// GET /api/v1/posts/456 - 获取特定文章
// GET /api/v1/posts/456/comments - 获取文章评论
type APIRoutes struct {
router *gin.Engine
}
func (api *APIRoutes) setupRoutes() {
v1 := api.router.Group("/api/v1")
// 用户资源
users := v1.Group("/users")
{
users.GET("", api.getUsers)
users.POST("", api.createUser)
users.GET("/:id", api.getUser)
users.PUT("/:id", api.updateUser)
users.DELETE("/:id", api.deleteUser)
// 嵌套资源
users.GET("/:id/posts", api.getUserPosts)
users.POST("/:id/posts", api.createUserPost)
}
// 文章资源
posts := v1.Group("/posts")
{
posts.GET("", api.getPosts)
posts.POST("", api.createPost)
posts.GET("/:id", api.getPost)
posts.PUT("/:id", api.updatePost)
posts.DELETE("/:id", api.deletePost)
// 评论子资源
posts.GET("/:id/comments", api.getPostComments)
posts.POST("/:id/comments", api.createComment)
}
}
通过表示操作资源(Resource Manipulation Through Representations) #
客户端通过资源的表示来操作资源。
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex"`
Email string `json:"email" gorm:"uniqueIndex"`
FullName string `json:"full_name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 创建用户的表示
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=20"`
Email string `json:"email" binding:"required,email"`
FullName string `json:"full_name" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
// 更新用户的表示
type UpdateUserRequest struct {
Email *string `json:"email,omitempty" binding:"omitempty,email"`
FullName *string `json:"full_name,omitempty"`
}
func createUserHandler(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 通过表示创建资源
user := User{
Username: req.Username,
Email: req.Email,
FullName: req.FullName,
}
// 密码哈希处理
hashedPassword, err := hashPassword(req.Password)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to process password"})
return
}
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create user"})
return
}
c.JSON(201, user)
}
自描述消息(Self-Descriptive Messages) #
每个消息都包含足够的信息来描述如何处理该消息。
func setupMiddleware(r *gin.Engine) {
// 内容类型中间件
r.Use(func(c *gin.Context) {
// 设置响应内容类型
c.Header("Content-Type", "application/json; charset=utf-8")
// 检查请求内容类型
if c.Request.Method == "POST" || c.Request.Method == "PUT" {
contentType := c.GetHeader("Content-Type")
if !strings.Contains(contentType, "application/json") {
c.JSON(415, gin.H{
"error": "Unsupported Media Type",
"message": "Content-Type must be application/json",
})
c.Abort()
return
}
}
c.Next()
})
}
// 错误响应结构
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code int `json:"code"`
Details map[string]string `json:"details,omitempty"`
}
func handleValidationError(c *gin.Context, err error) {
var details map[string]string
if validationErrors, ok := err.(validator.ValidationErrors); ok {
details = make(map[string]string)
for _, fieldError := range validationErrors {
details[fieldError.Field()] = getValidationErrorMessage(fieldError)
}
}
c.JSON(400, ErrorResponse{
Error: "Validation Failed",
Message: "Request validation failed",
Code: 400,
Details: details,
})
}
超媒体作为应用状态引擎(HATEOAS) #
响应应该包含链接,指导客户端如何进行下一步操作。
type UserResponse struct {
User
Links map[string]string `json:"_links"`
}
type UsersListResponse struct {
Users []UserResponse `json:"users"`
Meta MetaInfo `json:"meta"`
Links map[string]string `json:"_links"`
}
type MetaInfo struct {
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
}
func getUserHandler(c *gin.Context) {
userID := c.Param("id")
var user User
if err := db.First(&user, userID).Error; err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
// 构建 HATEOAS 链接
baseURL := fmt.Sprintf("%s://%s", getScheme(c), c.Request.Host)
userResponse := UserResponse{
User: user,
Links: map[string]string{
"self": fmt.Sprintf("%s/api/v1/users/%d", baseURL, user.ID),
"posts": fmt.Sprintf("%s/api/v1/users/%d/posts", baseURL, user.ID),
"edit": fmt.Sprintf("%s/api/v1/users/%d", baseURL, user.ID),
"delete": fmt.Sprintf("%s/api/v1/users/%d", baseURL, user.ID),
},
}
c.JSON(200, userResponse)
}
func getUsersHandler(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "10"))
var users []User
var total int64
offset := (page - 1) * perPage
db.Model(&User{}).Count(&total)
db.Offset(offset).Limit(perPage).Find(&users)
// 构建用户响应
userResponses := make([]UserResponse, len(users))
baseURL := fmt.Sprintf("%s://%s", getScheme(c), c.Request.Host)
for i, user := range users {
userResponses[i] = UserResponse{
User: user,
Links: map[string]string{
"self": fmt.Sprintf("%s/api/v1/users/%d", baseURL, user.ID),
"posts": fmt.Sprintf("%s/api/v1/users/%d/posts", baseURL, user.ID),
},
}
}
totalPages := int(math.Ceil(float64(total) / float64(perPage)))
// 构建分页链接
links := map[string]string{
"self": fmt.Sprintf("%s/api/v1/users?page=%d&per_page=%d", baseURL, page, perPage),
}
if page > 1 {
links["prev"] = fmt.Sprintf("%s/api/v1/users?page=%d&per_page=%d", baseURL, page-1, perPage)
links["first"] = fmt.Sprintf("%s/api/v1/users?page=1&per_page=%d", baseURL, perPage)
}
if page < totalPages {
links["next"] = fmt.Sprintf("%s/api/v1/users?page=%d&per_page=%d", baseURL, page+1, perPage)
links["last"] = fmt.Sprintf("%s/api/v1/users?page=%d&per_page=%d", baseURL, totalPages, perPage)
}
response := UsersListResponse{
Users: userResponses,
Meta: MetaInfo{
Total: int(total),
Page: page,
PerPage: perPage,
TotalPages: totalPages,
},
Links: links,
}
c.JSON(200, response)
}
5. 分层系统(Layered System) #
系统架构可以由多个层次组成,每一层只能看到与其交互的紧邻层。
// 分层架构示例
type UserController struct {
userService *UserService
}
type UserService struct {
userRepo *UserRepository
cache *CacheService
}
type UserRepository struct {
db *gorm.DB
}
// 控制器层
func (uc *UserController) GetUser(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid user ID"})
return
}
user, err := uc.userService.GetUserByID(uint(userID))
if err != nil {
if errors.Is(err, ErrUserNotFound) {
c.JSON(404, gin.H{"error": "User not found"})
return
}
c.JSON(500, gin.H{"error": "Internal server error"})
return
}
c.JSON(200, user)
}
// 服务层
func (us *UserService) GetUserByID(id uint) (*User, error) {
// 先尝试从缓存获取
if user, found := us.cache.Get(fmt.Sprintf("user:%d", id)); found {
return user.(*User), nil
}
// 从数据库获取
user, err := us.userRepo.FindByID(id)
if err != nil {
return nil, err
}
// 缓存结果
us.cache.Set(fmt.Sprintf("user:%d", id), user, 5*time.Minute)
return user, nil
}
// 数据访问层
func (ur *UserRepository) FindByID(id uint) (*User, error) {
var user User
if err := ur.db.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
6. 按需代码(Code on Demand) #
这是唯一的可选约束。服务器可以通过传输可执行代码来扩展客户端功能。
HTTP 方法的正确使用 #
RESTful API 应该正确使用 HTTP 方法来表示不同的操作。
GET - 获取资源 #
// 获取资源列表
func getUsers(c *gin.Context) {
// 支持查询参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
search := c.Query("search")
var users []User
query := db.Model(&User{})
if search != "" {
query = query.Where("username LIKE ? OR email LIKE ?",
"%"+search+"%", "%"+search+"%")
}
query.Offset((page - 1) * limit).Limit(limit).Find(&users)
c.JSON(200, gin.H{
"users": users,
"page": page,
"limit": limit,
})
}
// 获取单个资源
func getUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
c.JSON(200, user)
}
POST - 创建资源 #
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 验证数据
if err := validateUser(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create user"})
return
}
// 返回 201 Created 状态码
c.Header("Location", fmt.Sprintf("/api/v1/users/%d", user.ID))
c.JSON(201, user)
}
PUT - 完整更新资源 #
func updateUser(c *gin.Context) {
id := c.Param("id")
var existingUser User
if err := db.First(&existingUser, id).Error; err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
var updateData User
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// PUT 应该替换整个资源
updateData.ID = existingUser.ID
updateData.CreatedAt = existingUser.CreatedAt
if err := db.Save(&updateData).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to update user"})
return
}
c.JSON(200, updateData)
}
PATCH - 部分更新资源 #
func patchUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 只更新提供的字段
if err := db.Model(&user).Updates(updates).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to update user"})
return
}
c.JSON(200, user)
}
DELETE - 删除资源 #
func deleteUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
if err := db.Delete(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to delete user"})
return
}
// 返回 204 No Content
c.Status(204)
}
状态码的正确使用 #
RESTful API 应该使用适当的 HTTP 状态码来表示操作结果。
// 状态码常量
const (
StatusOK = 200
StatusCreated = 201
StatusAccepted = 202
StatusNoContent = 204
StatusBadRequest = 400
StatusUnauthorized = 401
StatusForbidden = 403
StatusNotFound = 404
StatusMethodNotAllowed = 405
StatusConflict = 409
StatusUnprocessableEntity = 422
StatusInternalServerError = 500
)
// 统一的错误处理
func handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrUserNotFound):
c.JSON(StatusNotFound, gin.H{
"error": "Resource not found",
"message": err.Error(),
})
case errors.Is(err, ErrValidationFailed):
c.JSON(StatusBadRequest, gin.H{
"error": "Validation failed",
"message": err.Error(),
})
case errors.Is(err, ErrUnauthorized):
c.JSON(StatusUnauthorized, gin.H{
"error": "Unauthorized",
"message": "Authentication required",
})
case errors.Is(err, ErrForbidden):
c.JSON(StatusForbidden, gin.H{
"error": "Forbidden",
"message": "Insufficient permissions",
})
default:
c.JSON(StatusInternalServerError, gin.H{
"error": "Internal server error",
"message": "An unexpected error occurred",
})
}
}
资源命名约定 #
良好的资源命名是 RESTful API 设计的重要组成部分。
// 良好的资源命名示例
func setupAPIRoutes(r *gin.Engine) {
api := r.Group("/api/v1")
// 使用复数名词表示资源集合
users := api.Group("/users")
{
users.GET("", getUsers) // GET /api/v1/users
users.POST("", createUser) // POST /api/v1/users
users.GET("/:id", getUser) // GET /api/v1/users/123
users.PUT("/:id", updateUser) // PUT /api/v1/users/123
users.DELETE("/:id", deleteUser) // DELETE /api/v1/users/123
// 嵌套资源
users.GET("/:id/posts", getUserPosts) // GET /api/v1/users/123/posts
users.POST("/:id/posts", createUserPost) // POST /api/v1/users/123/posts
}
posts := api.Group("/posts")
{
posts.GET("", getPosts)
posts.POST("", createPost)
posts.GET("/:id", getPost)
posts.PUT("/:id", updatePost)
posts.DELETE("/:id", deletePost)
// 评论作为子资源
posts.GET("/:id/comments", getPostComments)
posts.POST("/:id/comments", createComment)
posts.GET("/:id/comments/:comment_id", getComment)
posts.PUT("/:id/comments/:comment_id", updateComment)
posts.DELETE("/:id/comments/:comment_id", deleteComment)
}
// 非资源端点使用动词
api.POST("/auth/login", login)
api.POST("/auth/logout", logout)
api.POST("/auth/refresh", refreshToken)
api.POST("/users/:id/activate", activateUser)
api.POST("/posts/:id/publish", publishPost)
}
内容协商 #
RESTful API 应该支持内容协商,允许客户端指定期望的响应格式。
func contentNegotiationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
accept := c.GetHeader("Accept")
switch {
case strings.Contains(accept, "application/json"):
c.Set("responseType", "json")
case strings.Contains(accept, "application/xml"):
c.Set("responseType", "xml")
case strings.Contains(accept, "text/plain"):
c.Set("responseType", "text")
default:
c.Set("responseType", "json") // 默认 JSON
}
c.Next()
}
}
func respondWithData(c *gin.Context, data interface{}) {
responseType, _ := c.Get("responseType")
switch responseType {
case "xml":
c.XML(200, data)
case "text":
c.String(200, fmt.Sprintf("%+v", data))
default:
c.JSON(200, data)
}
}
通过遵循这些 RESTful 设计原则,你可以创建出清晰、一致、易于使用和维护的 API。这些原则不仅提高了 API 的质量,还增强了系统的可扩展性和互操作性。