⚡ Web 框架与 RESTful API
从 Gin 到 Fiber,选择合适的框架;设计规范的 RESTful API;掌握中间件、参数绑定、错误处理全流程。
1. Go Web 框架对比
🌿
Gin
~140k req/s
最流行,生态最完善,文档丰富。适合大多数业务服务。
🌊
Echo
~120k req/s
API 设计优雅,内置 Validator,适合 API 服务开发。
⚡
Fiber
~300k req/s
基于 fasthttp,性能极致,Express 风格 API,适合高并发场景。
| 特性 | Gin | Echo | Fiber | net/http |
|---|---|---|---|---|
| 性能 | 优秀 | 优秀 | 极佳 | 良好 |
| 生态 | 最丰富 | 丰富 | 增长中 | 标准库 |
| 学习曲线 | 低 | 低 | 很低(类Express) | 中 |
| 内存兼容 | 标准库兼容 | 标准库兼容 | ⚠️ fasthttp不兼容 | 标准 |
| 推荐场景 | 通用业务 | API服务 | 高并发/低延迟 | 工具/简单服务 |
2. Gin 框架实战
安装与基础路由
go get github.com/gin-gonic/gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// gin.Default() = gin.New() + Logger + Recovery 中间件
r := gin.Default()
// ─── 路由定义 ──────────────────────────────────────────
// 基础路由
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
// 路径参数
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"user_id": id})
})
// 查询参数: /search?q=go&page=1
r.GET("/search", func(c *gin.Context) {
q := c.Query("q")
page := c.DefaultQuery("page", "1")
c.JSON(200, gin.H{"query": q, "page": page})
})
// 路由组 (便于统一前缀和中间件)
api := r.Group("/api/v1")
{
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.GET("/users/:id", getUser)
api.PUT("/users/:id", updateUser)
api.DELETE("/users/:id", deleteUser)
}
r.Run(":8080")
}
请求绑定与校验
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// 请求结构体 (binding tag 驱动校验)
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Age int `json:"age" binding:"omitempty,min=0,max=150"`
Role string `json:"role" binding:"oneof=admin user guest"`
}
type UpdateUserRequest struct {
Name *string `json:"name" binding:"omitempty,min=2,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
}
func createUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 格式化校验错误
var ve validator.ValidationErrors
if errors.As(err, &ve) {
errs := make([]string, len(ve))
for i, e := range ve {
errs[i] = fmt.Sprintf("字段 %s: %s", e.Field(), e.Tag())
}
c.JSON(http.StatusBadRequest, gin.H{
"error": "参数校验失败",
"detail": errs,
})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 处理业务逻辑...
user := User{Name: req.Name, Email: req.Email}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"data": user,
})
}
// 路径参数 + 表单绑定
type UploadRequest struct {
Description string `form:"description"`
Category string `form:"category" binding:"required"`
}
func uploadFile(c *gin.Context) {
var req UploadRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "文件不存在"})
return
}
// 保存文件
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{"filename": file.Filename, "size": file.Size})
}
中间件系统
package main
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// ─── 认证中间件 ────────────────────────────────────────────────
func AuthMiddleware(secretKey string) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
token = c.Query("token")
}
token = strings.TrimPrefix(token, "Bearer ")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "未提供认证 Token",
})
return
}
// 验证 JWT (简化示例)
claims, err := validateJWT(token, secretKey)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Token 无效或已过期",
})
return
}
// 将用户信息存入 context
c.Set("user_id", claims.UserID)
c.Set("user_role", claims.Role)
c.Next() // 继续处理
}
}
// ─── 限流中间件 ────────────────────────────────────────────────
func RateLimitMiddleware(rps float64, burst int) gin.HandlerFunc {
limiter := rate.NewLimiter(rate.Limit(rps), burst)
return func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "请求频率超限",
"retry_after": "1s",
})
return
}
c.Next()
}
}
// ─── 请求 ID 中间件 ────────────────────────────────────────────
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateID() // UUID
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
// ─── CORS 中间件 ───────────────────────────────────────────────
func CORSMiddleware(allowOrigins []string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
allowed := false
for _, o := range allowOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if allowed {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Request-ID")
c.Header("Access-Control-Max-Age", "86400")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// ─── 路由注册 ──────────────────────────────────────────────────
func setupRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Recovery()) // panic 恢复
r.Use(RequestIDMiddleware()) // 请求 ID
r.Use(CORSMiddleware([]string{"*"})) // CORS
// 公开路由
public := r.Group("/api/v1")
public.Use(RateLimitMiddleware(100, 200)) // 100 req/s
{
public.POST("/auth/login", loginHandler)
public.POST("/auth/register", registerHandler)
public.GET("/health", healthHandler)
}
// 需要认证的路由
protected := r.Group("/api/v1")
protected.Use(AuthMiddleware("your-secret-key"))
protected.Use(RateLimitMiddleware(50, 100))
{
protected.GET("/users/me", getMeHandler)
protected.GET("/users", listUsers)
protected.POST("/users", createUser)
}
return r
}
3. RESTful API 设计规范
资源命名与 URL 设计
| 操作 | 方法 | URL | 状态码 |
|---|---|---|---|
| 获取用户列表 | GET | /api/v1/users | 200 |
| 创建用户 | POST | /api/v1/users | 201 |
| 获取指定用户 | GET | /api/v1/users/{id} | 200 |
| 全量更新用户 | PUT | /api/v1/users/{id} | 200 |
| 部分更新用户 | PATCH | /api/v1/users/{id} | 200 |
| 删除用户 | DELETE | /api/v1/users/{id} | 204 |
| 获取用户文章 | GET | /api/v1/users/{id}/posts | 200 |
统一响应格式
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 统一响应结构
type Response struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
Detail any `json:"detail,omitempty"`
}
type Meta struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
}
// 成功响应
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, Response{
Success: true,
Data: data,
RequestID: c.GetString("request_id"),
})
}
// 创建成功
func Created(c *gin.Context, data any) {
c.JSON(http.StatusCreated, Response{
Success: true,
Data: data,
RequestID: c.GetString("request_id"),
})
}
// 分页响应
func Paginated(c *gin.Context, data any, page, pageSize int, total int64) {
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
c.JSON(http.StatusOK, Response{
Success: true,
Data: data,
Meta: &Meta{
Page: page,
PageSize: pageSize,
Total: total,
TotalPages: totalPages,
},
RequestID: c.GetString("request_id"),
})
}
// 错误响应
func BadRequest(c *gin.Context, code, msg string, detail ...any) {
var d any
if len(detail) > 0 {
d = detail[0]
}
c.JSON(http.StatusBadRequest, Response{
Success: false,
Error: &ErrorInfo{Code: code, Message: msg, Detail: d},
RequestID: c.GetString("request_id"),
})
}
func Unauthorized(c *gin.Context, msg string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{
Success: false,
Error: &ErrorInfo{Code: "UNAUTHORIZED", Message: msg},
})
}
func InternalError(c *gin.Context) {
c.JSON(http.StatusInternalServerError, Response{
Success: false,
Error: &ErrorInfo{Code: "INTERNAL_ERROR", Message: "服务器内部错误"},
RequestID: c.GetString("request_id"),
})
}
4. 完整的用户 CRUD 示例
package handler
import (
"strconv"
"github.com/gin-gonic/gin"
"myapp/internal/service"
"myapp/internal/response"
)
type UserHandler struct {
svc service.UserService
}
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
// GET /api/v1/users?page=1&page_size=20&search=alice
func (h *UserHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
search := c.Query("search")
if page < 1 { page = 1 }
if pageSize < 1 || pageSize > 100 { pageSize = 20 }
users, total, err := h.svc.ListUsers(c.Request.Context(), service.ListUsersParams{
Page: page,
PageSize: pageSize,
Search: search,
})
if err != nil {
response.InternalError(c)
return
}
response.Paginated(c, users, page, pageSize, total)
}
// GET /api/v1/users/:id
func (h *UserHandler) Get(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "INVALID_ID", "用户 ID 格式错误")
return
}
user, err := h.svc.GetUser(c.Request.Context(), id)
if err != nil {
if errors.Is(err, service.ErrNotFound) {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
response.InternalError(c)
return
}
response.OK(c, user)
}
// POST /api/v1/users
func (h *UserHandler) Create(c *gin.Context) {
var req service.CreateUserInput
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "VALIDATION_ERROR", "参数校验失败", err.Error())
return
}
user, err := h.svc.CreateUser(c.Request.Context(), req)
if err != nil {
if errors.Is(err, service.ErrEmailExists) {
response.BadRequest(c, "EMAIL_EXISTS", "邮箱已被注册")
return
}
response.InternalError(c)
return
}
response.Created(c, user)
}
// PATCH /api/v1/users/:id
func (h *UserHandler) Update(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
// 确认只更新自己 (或管理员)
currentUserID := c.GetInt64("user_id")
if id != currentUserID {
response.Unauthorized(c, "无权修改其他用户")
return
}
var req service.UpdateUserInput
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "VALIDATION_ERROR", "参数错误", err.Error())
return
}
user, err := h.svc.UpdateUser(c.Request.Context(), id, req)
if err != nil {
response.InternalError(c)
return
}
response.OK(c, user)
}
// DELETE /api/v1/users/:id
func (h *UserHandler) Delete(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if err := h.svc.DeleteUser(c.Request.Context(), id); err != nil {
if errors.Is(err, service.ErrNotFound) {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
response.InternalError(c)
return
}
c.Status(204)
}
5. API 文档:Swagger/OpenAPI
# 安装 swag 工具
go install github.com/swaggo/swag/cmd/swag@latest
go get github.com/swaggo/gin-swagger
go get github.com/swaggo/files
// @title 用户管理 API
// @version 1.0
// @description 用户管理服务 API 文档
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
package main
// CreateUser godoc
// @Summary 创建用户
// @Description 创建一个新用户账户
// @Tags users
// @Accept json
// @Produce json
// @Param request body CreateUserRequest true "用户信息"
// @Success 201 {object} Response{data=User}
// @Failure 400 {object} Response
// @Failure 500 {object} Response
// @Security BearerAuth
// @Router /users [post]
func createUser(c *gin.Context) { /* ... */ }
// 在路由中注册文档
import (
ginSwagger "github.com/swaggo/gin-swagger"
swaggerFiles "github.com/swaggo/files"
_ "myapp/docs" // swagger 生成的文档
)
func setupRouter() *gin.Engine {
r := gin.Default()
// 访问 http://localhost:8080/swagger/index.html
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}
// 生成文档
// $ swag init -g cmd/server/main.go
REST API 设计要点
① 使用名词复数表示资源,不用动词(/users 不用 /getUsers) ② 版本化 API(/api/v1) ③ 统一错误响应格式 ④ 分页用 cursor 或 page-size,不用 offset-limit(大数据集) ⑤ 始终返回 Content-Type: application/json