Announcement

👇Official Account👇

图片

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Go RESTful API 最佳实践:构建高质量 Web 服务

什么是 RESTful API?

REST(Representational State Transfer)是一种软件架构风格,由 Roy Fielding 在 2000 年提出。RESTful API 遵循以下核心原则:

  • 资源导向:一切皆资源,通过 URL 标识
  • 统一接口:使用标准的 HTTP 方法(GET、POST、PUT、DELETE)
  • 无状态:每个请求独立,服务器不保存客户端状态
  • 可缓存:响应可以被客户端或中间层缓存
  • 分层系统:客户端不需要知道是否直接连接到服务器

项目结构

一个良好的项目结构是高质量 API 的基础:

api-project/
├── cmd/
│   └── api/
│       └── main.go          # 应用程序入口
├── internal/
│   ├── config/              # 配置管理
│   ├── handler/             # HTTP 处理器
│   ├── middleware/          # 中间件
│   ├── model/               # 数据模型
│   ├── repository/          # 数据访问层
│   ├── service/             # 业务逻辑层
│   └── router/              # 路由配置
├── pkg/
│   ├── response/            # 响应封装
│   ├── validator/           # 验证器
│   └── logger/              # 日志工具
├── docs/                    # API 文档
├── migrations/              # 数据库迁移
├── go.mod
├── go.sum
└── README.md

路由设计

使用 Gin 框架

go
package router

import (
    "github.com/gin-gonic/gin"
    "api/internal/handler"
    "api/internal/middleware"
)

func SetupRouter() *gin.Engine {
    r := gin.New()
    
    // 全局中间件
    r.Use(middleware.Logger())
    r.Use(middleware.Recovery())
    r.Use(middleware.CORS())
    
    // API 版本
    v1 := r.Group("/api/v1")
    {
        // 用户相关
        users := v1.Group("/users")
        {
            users.GET("", handler.ListUsers)
            users.POST("", handler.CreateUser)
            users.GET("/:id", handler.GetUser)
            users.PUT("/:id", handler.UpdateUser)
            users.DELETE("/:id", handler.DeleteUser)
        }
        
        // 文章相关
        articles := v1.Group("/articles")
        {
            articles.GET("", handler.ListArticles)
            articles.POST("", handler.CreateArticle)
            articles.GET("/:id", handler.GetArticle)
            articles.PUT("/:id", handler.UpdateArticle)
            articles.DELETE("/:id", handler.DeleteArticle)
        }
    }
    
    return r
}

RESTful URL 设计规范

操作HTTP 方法URL说明
列表GET/api/v1/users获取用户列表
创建POST/api/v1/users创建新用户
详情GET/api/v1/users/:id获取单个用户
更新PUT/PATCH/api/v1/users/:id更新用户信息
删除DELETE/api/v1/users/:id删除用户
搜索GET/api/v1/users?keyword=xxx搜索用户

请求处理

统一的请求验证

go
package validator

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

var validate *validator.Validate

func init() {
    validate = validator.New()
    binding.Validator = &defaultValidator{validate}
}

type defaultValidator struct {
    *validator.Validate
}

// 自定义验证规则示例
func RegisterCustomValidations() {
    validate.RegisterValidation("phone", validatePhone)
}

func validatePhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    // 手机号验证逻辑
    return len(phone) == 11
}

请求结构体定义

go
package model

// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
    Username string `json:"username" binding:"required,min=3,max=50"`
    Email    string `json:"email" binding:"required,email"`
    Phone    string `json:"phone" binding:"omitempty,phone"`
    Age      int    `json:"age" binding:"omitempty,min=0,max=150"`
}

// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
    Username string `json:"username" binding:"omitempty,min=3,max=50"`
    Email    string `json:"email" binding:"omitempty,email"`
    Phone    string `json:"phone" binding:"omitempty,phone"`
}

// ListUsersRequest 用户列表请求
type ListUsersRequest struct {
    Page     int    `form:"page" binding:"omitempty,min=1"`
    PageSize int    `form:"page_size" binding:"omitempty,min=1,max=100"`
    Keyword  string `form:"keyword" binding:"omitempty,max=100"`
}

响应设计

统一响应格式

go
package response

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

// Response 统一响应结构
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// PageData 分页数据结构
type PageData struct {
    List       interface{} `json:"list"`
    Total      int64       `json:"total"`
    Page       int         `json:"page"`
    PageSize   int         `json:"page_size"`
    TotalPages int         `json:"total_pages"`
}

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

// SuccessWithPage 成功响应(带分页)
func SuccessWithPage(c *gin.Context, list interface{}, total int64, page, pageSize int) {
    totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: "success",
        Data: PageData{
            List:       list,
            Total:      total,
            Page:       page,
            PageSize:   pageSize,
            TotalPages: totalPages,
        },
    })
}

// Error 错误响应
func Error(c *gin.Context, code int, message string) {
    c.JSON(code, Response{
        Code:    code,
        Message: message,
    })
}

// BadRequest 400 错误
func BadRequest(c *gin.Context, message string) {
    Error(c, http.StatusBadRequest, message)
}

// NotFound 404 错误
func NotFound(c *gin.Context, message string) {
    Error(c, http.StatusNotFound, message)
}

// InternalServerError 500 错误
func InternalServerError(c *gin.Context, message string) {
    Error(c, http.StatusInternalServerError, message)
}

处理器示例

go
package handler

import (
    "github.com/gin-gonic/gin"
    "api/internal/model"
    "api/internal/service"
    "api/pkg/response"
    "strconv"
)

type UserHandler struct {
    userService *service.UserService
}

func NewUserHandler() *UserHandler {
    return &UserHandler{
        userService: service.NewUserService(),
    }
}

// ListUsers 获取用户列表
func (h *UserHandler) ListUsers(c *gin.Context) {
    var req model.ListUsersRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    
    // 设置默认值
    if req.Page == 0 {
        req.Page = 1
    }
    if req.PageSize == 0 {
        req.PageSize = 10
    }
    
    users, total, err := h.userService.List(c.Request.Context(), &req)
    if err != nil {
        response.InternalServerError(c, err.Error())
        return
    }
    
    response.SuccessWithPage(c, users, total, req.Page, req.PageSize)
}

// CreateUser 创建用户
func (h *UserHandler) CreateUser(c *gin.Context) {
    var req model.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    
    user, err := h.userService.Create(c.Request.Context(), &req)
    if err != nil {
        response.InternalServerError(c, err.Error())
        return
    }
    
    response.Success(c, user)
}

// GetUser 获取用户详情
func (h *UserHandler) GetUser(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        response.BadRequest(c, "invalid user id")
        return
    }
    
    user, err := h.userService.GetByID(c.Request.Context(), id)
    if err != nil {
        response.NotFound(c, "user not found")
        return
    }
    
    response.Success(c, user)
}

// UpdateUser 更新用户
func (h *UserHandler) UpdateUser(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        response.BadRequest(c, "invalid user id")
        return
    }
    
    var req model.UpdateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, err.Error())
        return
    }
    
    user, err := h.userService.Update(c.Request.Context(), id, &req)
    if err != nil {
        response.InternalServerError(c, err.Error())
        return
    }
    
    response.Success(c, user)
}

// DeleteUser 删除用户
func (h *UserHandler) DeleteUser(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        response.BadRequest(c, "invalid user id")
        return
    }
    
    if err := h.userService.Delete(c.Request.Context(), id); err != nil {
        response.InternalServerError(c, err.Error())
        return
    }
    
    response.Success(c, nil)
}

错误处理

自定义错误类型

go
package errors

import (
    "errors"
    "fmt"
)

// AppError 应用错误
type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// 预定义错误
var (
    ErrNotFound     = NewAppError(404, "resource not found")
    ErrBadRequest   = NewAppError(400, "bad request")
    ErrUnauthorized = NewAppError(401, "unauthorized")
    ErrForbidden    = NewAppError(403, "forbidden")
    ErrInternal     = NewAppError(500, "internal server error")
)

func NewAppError(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

func Wrap(err error, code int, message string) *AppError {
    return &AppError{Code: code, Message: message, Err: err}
}

func IsNotFound(err error) bool {
    var appErr *AppError
    if errors.As(err, &appErr) {
        return appErr.Code == 404
    }
    return false
}

中间件

日志中间件

go
package middleware

import (
    "github.com/gin-gonic/gin"
    "log"
    "time"
)

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        raw := c.Request.URL.RawQuery
        
        // 处理请求
        c.Next()
        
        // 记录日志
        latency := time.Since(start)
        clientIP := c.ClientIP()
        method := c.Request.Method
        statusCode := c.Writer.Status()
        
        if raw != "" {
            path = path + "?" + raw
        }
        
        log.Printf("[%s] %s %s %d %v",
            clientIP,
            method,
            path,
            statusCode,
            latency,
        )
    }
}

认证中间件

go
package middleware

import (
    "github.com/gin-gonic/gin"
    "api/pkg/jwt"
    "api/pkg/response"
    "strings"
)

func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            response.Error(c, 401, "authorization header required")
            c.Abort()
            return
        }
        
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            response.Error(c, 401, "invalid authorization header format")
            c.Abort()
            return
        }
        
        claims, err := jwt.ParseToken(parts[1])
        if err != nil {
            response.Error(c, 401, "invalid token")
            c.Abort()
            return
        }
        
        // 将用户信息存入上下文
        c.Set("userID", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

CORS 中间件

go
package middleware

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

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        
        c.Next()
    }
}

API 文档

使用 Swagger

go
package main

// @title           Example API
// @version         1.0
// @description     This is a sample server.
// @termsOfService  http://swagger.io/terms/

// @contact.name   API Support
// @contact.url    http://www.swagger.io/support
// @contact.email  support@swagger.io

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html

// @host      localhost:8080
// @BasePath  /api/v1

// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization

func main() {
    r := router.SetupRouter()
    r.Run(":8080")
}

测试

单元测试

go
package handler

import (
    "bytes"
    "encoding/json"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestCreateUser(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    handler := NewUserHandler()
    r.POST("/users", handler.CreateUser)
    
    // 构造请求
    reqBody := map[string]interface{}{
        "username": "testuser",
        "email":    "test@example.com",
    }
    jsonBody, _ := json.Marshal(reqBody)
    
    req, _ := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonBody))
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    assert.Equal(t, http.StatusOK, w.Code)
}

性能优化

连接池配置

go
package config

import (
    "database/sql"
    "time"
    _ "github.com/go-sql-driver/mysql"
)

func NewDB() (*sql.DB, error) {
    db, err := sql.Open("mysql", "user:password@/dbname")
    if err != nil {
        return nil, err
    }
    
    // 连接池配置
    db.SetMaxOpenConns(25)        // 最大打开连接数
    db.SetMaxIdleConns(10)        // 最大空闲连接数
    db.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
    
    return db, nil
}

部署

Dockerfile

dockerfile
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o api cmd/api/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/api .

EXPOSE 8080
CMD ["./api"]

总结

构建高质量的 RESTful API 需要关注以下关键点:

  1. 良好的项目结构:分层清晰,职责单一
  2. 统一的接口规范:RESTful 设计,版本控制
  3. 完善的错误处理:自定义错误类型,统一响应格式
  4. 合理的中间件设计:日志、认证、CORS 等
  5. 完善的文档:使用 Swagger 自动生成
  6. 充分的测试:单元测试、集成测试
  7. 性能优化:连接池、缓存、异步处理

相关文章推荐:

上次更新于: