导航菜单

项目实战:构建 RESTful API

本章将综合前面所学知识,从零构建一个结构清晰、功能完整的 RESTful API 项目。

项目结构与分层设计

Go 项目分层架构

Go 社区推荐的目录结构遵循”关注点分离”原则:

myapp/
├── cmd/
│   └── api/
│       └── main.go          # 程序入口
├── internal/                  # 私有包,外部不可导入
│   ├── handler/               # HTTP 处理层(Controller)
│   ├── service/               # 业务逻辑层
│   ├── repository/            # 数据访问层(DAO)
│   ├── model/                 # 数据模型
│   ├── middleware/             # 中间件
│   └── config/                # 配置管理
├── pkg/                       # 可被外部导入的公共包
├── go.mod
├── go.sum
├── config.yaml                # 配置文件
└── Makefile                   # 构建脚本

数据模型

// internal/model/todo.go
package model

import "time"

type Todo struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Title     string    `json:"title" gorm:"size:200;not null"`
    Completed bool      `json:"completed" gorm:"default:false"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

Repository 层 —— 数据访问

// internal/repository/todo.go
package repository

import (
    "myapp/internal/model"
    "gorm.io/gorm"
)

type TodoRepository struct {
    db *gorm.DB
}

func NewTodoRepository(db *gorm.DB) *TodoRepository {
    return &TodoRepository{db: db}
}

func (r *TodoRepository) Create(todo *model.Todo) error {
    return r.db.Create(todo).Error
}

func (r *TodoRepository) GetAll() ([]model.Todo, error) {
    var todos []model.Todo
    err := r.db.Order("created_at DESC").Find(&todos).Error
    return todos, err
}

func (r *TodoRepository) GetByID(id uint) (*model.Todo, error) {
    var todo model.Todo
    err := r.db.First(&todo, id).Error
    return &todo, err
}

func (r *TodoRepository) Update(todo *model.Todo) error {
    return r.db.Save(todo).Error
}

func (r *TodoRepository) Delete(id uint) error {
    return r.db.Delete(&model.Todo{}, id).Error
}

Service 层 —— 业务逻辑

// internal/service/todo.go
package service

import (
    "errors"
    "myapp/internal/model"
    "myapp/internal/repository"
)

type TodoService struct {
    repo *repository.TodoRepository
}

func NewTodoService(repo *repository.TodoRepository) *TodoService {
    return &TodoService{repo: repo}
}

func (s *TodoService) Create(title string) (*model.Todo, error) {
    if title == "" {
        return nil, errors.New("标题不能为空")
    }
    todo := &model.Todo{Title: title}
    if err := s.repo.Create(todo); err != nil {
        return nil, err
    }
    return todo, nil
}

func (s *TodoService) GetAll() ([]model.Todo, error) {
    return s.repo.GetAll()
}

func (s *TodoService) Toggle(id uint) (*model.Todo, error) {
    todo, err := s.repo.GetByID(id)
    if err != nil {
        return nil, errors.New("Todo 不存在")
    }
    todo.Completed = !todo.Completed
    s.repo.Update(todo)
    return todo, nil
}

func (s *TodoService) Delete(id uint) error {
    return s.repo.Delete(id)
}

Handler 层 —— 请求处理

// internal/handler/todo.go
package handler

import (
    "encoding/json"
    "myapp/internal/model"
    "myapp/internal/service"
    "net/http"
    "strconv"
)

type TodoHandler struct {
    svc *service.TodoService
}

func NewTodoHandler(svc *service.TodoService) *TodoHandler {
    return &TodoHandler{svc: svc}
}

type CreateRequest struct {
    Title string `json:"title"`
}

func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "无效的请求体", http.StatusBadRequest)
        return
    }
    todo, err := h.svc.Create(req.Title)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(todo)
}

func (h *TodoHandler) GetAll(w http.ResponseWriter, r *http.Request) {
    todos, err := h.svc.GetAll()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(todos)
}

func (h *TodoHandler) Toggle(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.ParseUint(r.PathValue("id"), 10, 64)
    todo, err := h.svc.Toggle(uint(id))
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    json.NewEncoder(w).Encode(todo)
}

func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.ParseUint(r.PathValue("id"), 10, 64)
    if err := h.svc.Delete(uint(id)); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

路由注册

// cmd/api/main.go
func main() {
    // ... 初始化 db、repo、svc、handler ...

    mux := http.NewServeMux()

    // 注册路由
    mux.HandleFunc("POST /api/todos", todoHandler.Create)
    mux.HandleFunc("GET /api/todos", todoHandler.GetAll)
    mux.HandleFunc("PATCH /api/todos/{id}", todoHandler.Toggle)
    mux.HandleFunc("DELETE /api/todos/{id}", todoHandler.Delete)
}

优雅关闭

func main() {
    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 启动服务器(非阻塞)
    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("服务器错误: %v", err)
        }
    }()

    // 监听中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("收到关闭信号,开始优雅关闭...")

    // 给 5 秒超时处理完正在进行的请求
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("服务器强制关闭: %v", err)
    }
    log.Println("服务器已关闭")
}

练习题

练习 1

基于本章的分层架构,为 Todo 项目添加一个”按标题搜索”的功能,要求贯穿 Repository → Service → Handler 三层。

参考答案 (2 个标签)
分层架构 RESTful API

Repository 层

func (r *TodoRepository) Search(keyword string) ([]model.Todo, error) {
    var todos []model.Todo
    err := r.db.Where("title LIKE ?", "%"+keyword+"%").Order("created_at DESC").Find(&todos).Error
    return todos, err
}

Service 层

func (s *TodoService) Search(keyword string) ([]model.Todo, error) {
    if keyword == "" {
        return s.repo.GetAll()
    }
    return s.repo.Search(keyword)
}

Handler 层

func (h *TodoHandler) Search(w http.ResponseWriter, r *http.Request) {
    keyword := r.URL.Query().Get("q")
    todos, err := h.svc.Search(keyword)
    // ... 处理响应
}

路由

mux.HandleFunc("GET /api/todos/search", todoHandler.Search)

搜索