Go语言API设计清单:RESTful检查
Go语言API设计清单RESTful检查良好的API设计是提升开发者体验的关键。本文详细介绍Go语言中RESTful API设计的最佳实践和检查项。一、RESTful设计原则1.1 资源导向的URL设计RESTful API以资源为核心URL应该清晰地表达资源而不是动作。// 不好的设计使用动词 GET /getUser?id1 POST /createUser PUT /updateUser DELETE /deleteUser?id1 // 好的设计使用名词表示资源 GET /users/1 // 获取用户 POST /users // 创建用户 PUT /users/1 // 更新用户 DELETE /users/1 // 删除用户1.2 分层结构URL应该反映资源的层级关系// 用户资源下的订单资源 GET /users/1/orders // 获取用户1的所有订单 GET /users/1/orders/2 // 获取用户1的订单2 // 博客系统 GET /articles // 获取所有文章 GET /articles/123 // 获取ID为123的文章 GET /articles/123/comments // 获取文章123的评论二、HTTP方法规范2.1 标准HTTP方法使用方法用途幂等性安全性GET获取资源是是POST创建资源否否PUT完整更新资源是否PATCH部分更新资源否否DELETE删除资源是否2.2 Go语言HTTP方法实现import ( encoding/json net/http ) type UserHandler struct { service UserService } // GET /users func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) { users, err : h.service.ListUsers() if err ! nil { writeError(w, err) return } writeJSON(w, http.StatusOK, users) } // GET /users/:id func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) { id : extractID(r) user, err : h.service.GetUser(id) if err ! nil { if err ErrNotFound { writeError(w, ErrNotFound) return } writeError(w, err) return } writeJSON(w, http.StatusOK, user) } // POST /users func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err : json.NewDecoder(r.Body).Decode(req); err ! nil { writeError(w, err) return } user, err : h.service.CreateUser(req) if err ! nil { writeError(w, err) return } writeJSON(w, http.StatusCreated, user) } // PUT /users/:id func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { id : extractID(r) var req UpdateUserRequest if err : json.NewDecoder(r.Body).Decode(req); err ! nil { writeError(w, err) return } user, err : h.service.UpdateUser(id, req) if err ! nil { writeError(w, err) return } writeJSON(w, http.StatusOK, user) } // DELETE /users/:id func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) { id : extractID(r) if err : h.service.DeleteUser(id); err ! nil { writeError(w, err) return } w.WriteHeader(http.StatusNoContent) }三、HTTP状态码规范3.1 常用状态码状态码含义使用场景200 OK请求成功GET/PUT/DELETE成功201 Created资源创建成功POST创建新资源204 No Content无返回内容DELETE成功无返回400 Bad Request请求参数错误参数验证失败401 Unauthorized未认证需要登录403 Forbidden无权限无权访问404 Not Found资源不存在资源未找到409 Conflict资源冲突重复创建等422 Unprocessable Entity验证错误业务逻辑验证失败500 Internal Server Error服务器错误未知错误3.2 Go语言状态码实现import ( net/http ) // 成功响应 func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set(Content-Type, application/json) w.WriteHeader(status) json.NewEncoder(w).Encode(data) } // 错误响应 type APIError struct { Code int json:code Message string json:message Details string json:details,omitempty } func writeError(w http.ResponseWriter, err error) { var status int var message string switch err { case ErrNotFound: status http.StatusNotFound message 资源不存在 case ErrUnauthorized: status http.StatusUnauthorized message 未认证 case ErrForbidden: status http.StatusForbidden message 无权限 case ErrValidation: status http.StatusBadRequest message 参数验证失败 default: status http.StatusInternalServerError message 服务器内部错误 } writeJSON(w, status, APIError{ Code: status, Message: message, }) }四、版本控制4.1 URL版本控制最常用的版本控制方式通过URL路径区分版本。// v1版本 http.HandleFunc(/api/v1/users, v1Handler) // v2版本 http.HandleFunc(/api/v2/users, v2Handler) // 实际路由注册 type APIServer struct { v1Handler *V1Handler v2Handler *V2Handler } func (s *APIServer) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc(/api/v1/users, s.v1Handler.HandleUsers) mux.HandleFunc(/api/v1/users/, s.v1Handler.HandleUser) mux.HandleFunc(/api/v2/users, s.v2Handler.HandleUsers) mux.HandleFunc(/api/v2/users/, s.v2Handler.HandleUser) }4.2 Header版本控制通过Accept header指定版本号。func versionMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { accept : r.Header.Get(Accept) version : extractVersion(accept) // 如 application/vnd.api.v2json ctx : context.WithValue(r.Context(), version, version) next.ServeHTTP(w, r.WithContext(ctx)) }) } // 客户端请求示例 // Accept: application/vnd.myapi.v2json五、错误响应格式5.1 统一错误响应结构type ErrorResponse struct { Error ErrorDetail json:error } type ErrorDetail struct { Code string json:code Message string json:message Details map[string]string json:details,omitempty TraceID string json:trace_id,omitempty Timestamp string json:timestamp } // 详细错误示例 { error: { code: VALIDATION_ERROR, message: 请求参数验证失败, details: { email: 邮箱格式不正确, password: 密码长度不足8位 }, trace_id: abc123def456, timestamp: 2024-01-15T10:30:00Z } }5.2 错误处理中间件type AppError struct { Code string Message string Status int Details map[string]string } func (e *AppError) Error() string { return e.Message } func recoveryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err : recover(); err ! nil { traceID : generateTraceID() log.Printf(panic recovered: %v, trace_id: %s, err, traceID) writeJSON(w, http.StatusInternalServerError, ErrorResponse{ Error: ErrorDetail{ Code: INTERNAL_ERROR, Message: 服务器内部错误, TraceID: traceID, Timestamp: time.Now().UTC().Format(time.RFC3339), }, }) } }() next.ServeHTTP(w, r) }) }六、分页实现6.1 常见的分页方式偏移分页Offset Paginationtype PaginationRequest struct { Page int json:page PageSize int json:page_size } type PaginatedResponse struct { Data interface{} json:data Pagination Pagination json:pagination } type Pagination struct { Page int json:page PageSize int json:page_size TotalItems int64 json:total_items TotalPages int json:total_pages } // GET /users?page1page_size20 func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) { page, _ : strconv.Atoi(r.URL.Query().Get(page)) pageSize, _ : strconv.Atoi(r.URL.Query().Get(page_size)) if page 1 { page 1 } if pageSize 1 || pageSize 100 { pageSize 20 } offset : (page - 1) * pageSize users, total, err : h.service.ListUsers(offset, pageSize) if err ! nil { writeError(w, err) return } totalPages : int(total) / pageSize if int(total)%pageSize 0 { totalPages } writeJSON(w, http.StatusOK, PaginatedResponse{ Data: users, Pagination: Pagination{ Page: page, PageSize: pageSize, TotalItems: total, TotalPages: totalPages, }, }) }游标分页Cursor Paginationtype CursorRequest struct { Cursor string json:cursor Limit int json:limit } type CursorResponse struct { Data interface{} json:data NextCursor string json:next_cursor,omitempty HasMore bool json:has_more } // GET /users?cursorabc123limit20 func (h *UserHandler) ListUsersWithCursor(w http.ResponseWriter, r *http.Request) { cursor : r.URL.Query().Get(cursor) limit, _ : strconv.Atoi(r.URL.Query().Get(limit)) if limit 1 || limit 100 { limit 20 } users, nextCursor, hasMore, err : h.service.ListUsersWithCursor(cursor, limit) if err ! nil { writeError(w, err) return } writeJSON(w, http.StatusOK, CursorResponse{ Data: users, NextCursor: nextCursor, HasMore: hasMore, }) }七、认证与授权7.1 JWT认证type AuthHandler struct { jwtSecret []byte } type Claims struct { UserID int64 json:user_id Username string json:username jwt.RegisteredClaims } func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { var req LoginRequest if err : json.NewDecoder(r.Body).Decode(req); err ! nil { writeError(w, err) return } user, err : h.service.Authenticate(req.Username, req.Password) if err ! nil { writeError(w, ErrUnauthorized) return } // 生成JWT token expiresAt : time.Now().Add(24 * time.Hour) claims : Claims{ UserID: user.ID, Username: user.Username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token : jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err : token.SignedString(h.jwtSecret) if err ! nil { writeError(w, err) return } writeJSON(w, http.StatusOK, map[string]interface{}{ token: tokenString, expires_at: expiresAt, }) } // JWT验证中间件 func (h *AuthHandler) AuthMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader : r.Header.Get(Authorization) if authHeader { writeError(w, ErrUnauthorized) return } parts : strings.SplitN(authHeader, , 2) if len(parts) ! 2 || parts[0] ! Bearer { writeError(w, ErrUnauthorized) return } tokenString : parts[1] claims : Claims{} token, err : jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return h.jwtSecret, nil }) if err ! nil || !token.Valid { writeError(w, ErrUnauthorized) return } ctx : context.WithValue(r.Context(), user_id, claims.UserID) ctx context.WithValue(ctx, username, claims.Username) next.ServeHTTP(w, r.WithContext(ctx)) }) } }7.2 RBAC授权type Role string const ( RoleAdmin Role admin RoleUser Role user RoleGuest Role guest ) // 权限检查 func RequireRole(roles ...Role) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole : r.Context().Value(user_role).(Role) for _, role : range roles { if userRole role { next.ServeHTTP(w, r) return } } writeError(w, ErrForbidden) }) } } // 使用示例 mux.HandleFunc(/admin/users, requireAuth( RequireRole(RoleAdmin)(handleAdminUsers), ), )八、检查清单总结URL设计使用名词表示资源而非动词URL使用小写字母和连字符资源层级关系清晰避免过深的嵌套超过3层考虑扁平化HTTP方法GET用于获取资源POST用于创建资源PUT用于完整更新资源PATCH用于部分更新资源DELETE用于删除资源状态码正确使用2xx状态码正确使用4xx客户端错误码正确使用5xx服务器错误码不返回200但实际有错误版本控制URL包含版本号旧版本API保持兼容或提供迁移指引版本变更有清晰的更新日志错误处理统一错误响应格式错误信息对客户端友好生产环境不泄露内部细节提供trace_id便于排查认证授权使用JWT或其他标准认证方式敏感操作需要授权验证Token有过期时间实现基于角色的访问控制良好的API设计不仅提升开发者体验还能减少客户端与服务端的沟通成本。建议在开发前制定详细的API规范并保持API的向后兼容性。