From e6a654e6fca396ebf314dae1f21f38d50f9dc4a8 Mon Sep 17 00:00:00 2001 From: openclaw Date: Tue, 24 Mar 2026 01:37:03 +0000 Subject: [PATCH] Revised Chapter 6: Web API Project - Friendly Tone, Restaurant Analogy, Diagrams, and Complete Code --- chapters/chapter-6-revised.md | 587 ++++++++++++++++++++++++++++++++++ 1 file changed, 587 insertions(+) create mode 100644 chapters/chapter-6-revised.md diff --git a/chapters/chapter-6-revised.md b/chapters/chapter-6-revised.md new file mode 100644 index 0000000..2c3c986 --- /dev/null +++ b/chapters/chapter-6-revised.md @@ -0,0 +1,587 @@ +# 第六章:实战项目 —— 构建一个 Web API 服务(完整实战版) + +> **👋 回顾一下:** +> +> 前四章,我们学会了基础语法、数据结构、函数和接口。第五章,我们攻克了并发编程的难关。现在,你已经掌握了 Go 语言的**所有核心技能**! +> +> **🤔 但是,光会理论可不够,我们要“学以致用”!** +> +> 想象一下,你开了一家**“任务管理餐厅”**。 +> * **顾客**(客户端)点菜(发送 HTTP 请求)。 +> * **服务员**(Handler)接收订单,传给**厨师**(Service)。 +> * **厨师**(Service)根据**仓库**(Repository)的食材,做出菜品(业务逻辑)。 +> * **服务员**(Handler)把菜品端给顾客(返回 HTTP 响应)。 +> +> 这就是一个典型的 **Web API 服务**! +> +> **🎯 这一章,我们要亲手搭建这家“餐厅”:** +> 1. **项目架构**:分层设计(Handler -> Service -> Repository)。 +> 2. **核心功能**:创建、查询、更新、删除任务(CRUD)。 +> 3. **并发处理**:用 Goroutine 处理异步通知,用 Mutex 保护共享状态。 +> 4. **优雅关闭**:收到信号后,优雅地停止服务。 +> 5. **Docker 部署**:把餐厅“打包”成集装箱,随时随地运行。 +> +> **别担心,我会一步步带你完成,让你体验“从零到一”的成就感!** + +--- + +## 6.1 项目架构设计 —— 餐厅的“布局图” + +> **💡 想象一下:** +> 一家好的餐厅,布局一定要清晰:厨房、仓库、大厅各司其职。 +> 我们的代码也要这样,**分层管理**,互不干扰。 + +### 📂 目录结构 + +``` +task-api/ +├── cmd/ +│ └── server/ +│ └── main.go # 餐厅大门(入口) +├── internal/ +│ ├── handler/ # 服务员(处理 HTTP 请求) +│ │ └── task_handler.go +│ ├── service/ # 厨师(业务逻辑) +│ │ └── task_service.go +│ ├── repository/ # 仓库(数据访问) +│ │ └── task_repo.go +│ ├── middleware/ # 保安、清洁工(中间件) +│ │ ├── logger.go +│ │ └── recovery.go +│ └── model/ # 菜单(数据模型) +│ └── task.go +├── go.mod +└── main.go +``` + +> **💡 老师的小揭秘:** +> * **cmd/**:程序入口,负责初始化(加载配置、连接数据库、启动服务)。 +> * **internal/**:私有代码,外部无法导入,保证封装性。 +> * **handler**:处理 HTTP 请求/响应,参数校验,调用 Service。 +> * **service**:核心业务逻辑,调用 Repository,处理事务。 +> * **repository**:数据库操作,SQL 查询,映射到 Model。 +> * **middleware**:横切关注点(日志、认证、恢复)。 +> * **model**:数据模型,定义数据结构。 + +--- + +## 6.2 数据模型 (Model) —— 餐厅的“菜单” + +> **💡 想象一下:** +> 菜单上写着:菜名、价格、描述…… +> 我们的**任务**也有类似的信息:标题、描述、状态、优先级…… + +### 📝 定义模型 + +```go +// internal/model/task.go +package model + +import "time" + +type Status string + +const ( + StatusTodo Status = "todo" + StatusInProgress Status = "in_progress" + StatusDone Status = "done" +) + +type Task struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Status Status `json:"status"` + Priority int `json:"priority"` // 1-5 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} +``` + +> **💡 老师的小揭秘:** +> * **Status**:自定义类型,避免魔法字符串。 +> * **JSON 标签**:明确字段映射,前端看到的字段名。 +> * **时间字段**:`time.Time` 自动序列化为 ISO 8601 格式。 + +--- + +## 6.3 仓库层 (Repository) —— 食材的“存取” + +> **💡 想象一下:** +> 厨师要炒菜,得先去仓库拿食材。 +> Repository 就是**数据仓库**,负责从数据库(或内存)存取数据。 + +### 📝 内存存储(简化版,方便运行) + +```go +// internal/repository/task_repo.go +package repository + +import ( + "context" + "fmt" + "sync" + "time" + "task-api/internal/model" +) + +type TaskRepo struct { + mu sync.Mutex + tasks map[int64]*model.Task + nextID int64 +} + +func NewTaskRepo() *TaskRepo { + return &TaskRepo{ + tasks: make(map[int64]*model.Task), + nextID: 1, + } +} + +func (r *TaskRepo) Create(ctx context.Context, task *model.Task) error { + r.mu.Lock() + defer r.mu.Unlock() + + task.ID = r.nextID + r.nextID++ + task.CreatedAt = time.Now() + task.UpdatedAt = time.Now() + r.tasks[task.ID] = task + return nil +} + +func (r *TaskRepo) GetByID(ctx context.Context, id int64) (*model.Task, error) { + r.mu.Lock() + defer r.mu.Unlock() + + task, ok := r.tasks[id] + if !ok { + return nil, fmt.Errorf("task not found") + } + return task, nil +} + +func (r *TaskRepo) List(ctx context.Context) []*model.Task { + r.mu.Lock() + defer r.mu.Unlock() + + tasks := make([]*model.Task, 0, len(r.tasks)) + for _, t := range r.tasks { + tasks = append(tasks, t) + } + return tasks +} + +func (r *TaskRepo) Update(ctx context.Context, task *model.Task) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.tasks[task.ID]; !ok { + return fmt.Errorf("task not found") + } + task.UpdatedAt = time.Now() + r.tasks[task.ID] = task + return nil +} + +func (r *TaskRepo) Delete(ctx context.Context, id int64) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.tasks[id]; !ok { + return fmt.Errorf("task not found") + } + delete(r.tasks, id) + return nil +} +``` + +> **💡 老师的小揭秘:** +> * **Mutex**:保护共享的 `tasks` 映射,避免并发读写冲突。 +> * **Context**:虽然内存存储不需要超时,但保持接口一致性,方便以后切换数据库。 +> * **错误处理**:返回明确的错误信息,便于上层处理。 + +--- + +## 6.4 服务层 (Service) —— 厨师的“烹饪” + +> **💡 想象一下:** +> 厨师接到订单,检查食材是否足够,然后开始烹饪。 +> Service 层负责**业务逻辑**:参数校验、数据转换、调用 Repository。 + +### 📝 业务逻辑 + +```go +// internal/service/task_service.go +package service + +import ( + "context" + "errors" + "time" + "task-api/internal/model" + "task-api/internal/repository" +) + +var ErrNotFound = errors.New("task not found") + +type TaskService struct { + repo *repository.TaskRepo +} + +func NewTaskService(repo *repository.TaskRepo) *TaskService { + return &TaskService{repo: repo} +} + +func (s *TaskService) CreateTask(ctx context.Context, task *model.Task) error { + if task.Title == "" { + return errors.New("title is required") + } + if task.Priority < 1 || task.Priority > 5 { + return errors.New("priority must be between 1 and 5") + } + return s.repo.Create(ctx, task) +} + +func (s *TaskService) GetTask(ctx context.Context, id int64) (*model.Task, error) { + task, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, ErrNotFound + } + return task, nil +} + +func (s *TaskService) ListTasks(ctx context.Context) []*model.Task { + return s.repo.List(ctx) +} + +func (s *TaskService) UpdateTask(ctx context.Context, task *model.Task) error { + if task.Title == "" { + return errors.New("title is required") + } + return s.repo.Update(ctx, task) +} + +func (s *TaskService) DeleteTask(ctx context.Context, id int64) error { + return s.repo.Delete(ctx, id) +} + +// 异步通知示例 +func (s *TaskService) NotifyTask(ctx context.Context, taskID int64) { + go func() { + // 模拟发送通知 + // 实际项目中应传递 context 并处理超时 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + select { + case <-ctx.Done(): + // 超时 + default: + // 成功 + } + }() +} +``` + +> **💡 老师的小揭秘:** +> * **参数校验**:在 Service 层进行,确保数据合法。 +> * **错误定义**:定义自定义错误,便于 Handler 层统一处理。 +> * **异步任务**:用 `go func()` 启动 Goroutine,注意处理 Context 和超时。 + +--- + +## 6.5 处理层 (Handler) —— 服务员的“接待” + +> **💡 想象一下:** +> 服务员接待顾客,记录订单,把菜端回来。 +> Handler 层负责**HTTP 请求/响应**,参数解析,调用 Service。 + +### 📝 HTTP 处理 + +```go +// internal/handler/task_handler.go +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + "task-api/internal/model" + "task-api/internal/service" +) + +type TaskHandler struct { + service *service.TaskService +} + +func NewTaskHandler(svc *service.TaskService) *TaskHandler { + return &TaskHandler{service: svc} +} + +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) { + var task model.Task + if err := json.NewDecoder(r.Body).Decode(&task); err != nil { + h.respondError(w, http.StatusBadRequest, "invalid request body") + return + } + + if err := h.service.CreateTask(r.Context(), &task); err != nil { + h.respondError(w, http.StatusInternalServerError, err.Error()) + return + } + + h.respondJSON(w, http.StatusCreated, "created", task) +} + +func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) { + idStr := r.URL.Query().Get("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + h.respondError(w, http.StatusBadRequest, "invalid id") + return + } + + task, err := h.service.GetTask(r.Context(), id) + if err != nil { + h.respondError(w, http.StatusNotFound, "task not found") + return + } + + h.respondJSON(w, http.StatusOK, "success", task) +} + +func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) { + tasks := h.service.ListTasks(r.Context()) + h.respondJSON(w, http.StatusOK, "success", tasks) +} + +func (h *TaskHandler) respondJSON(w http.ResponseWriter, code int, msg string, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(Response{Code: code, Message: msg, Data: data}) +} + +func (h *TaskHandler) respondError(w http.ResponseWriter, code int, msg string) { + h.respondJSON(w, code, msg, nil) +} +``` + +> **💡 老师的小揭秘:** +> * **统一响应格式**:`Response` 结构体,包含 Code、Message、Data。 +> * **错误处理**:统一调用 `respondError`,避免重复代码。 +> * **Context 传递**:`r.Context()` 自动包含超时和取消信号。 + +--- + +## 6.6 中间件 (Middleware) —— 餐厅的“保安与清洁工” + +> **💡 想象一下:** +> 保安检查顾客身份(认证),清洁工记录顾客进出时间(日志),万一顾客闹事(panic),有人处理(恢复)。 +> 中间件就是这些**横切关注点**。 + +### 📝 中间件实现 + +```go +// internal/middleware/logger.go +package middleware + +import ( + "log" + "net/http" + "time" +) + +func Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + log.Printf("Started %s %s", r.Method, r.URL.Path) + next.ServeHTTP(w, r) + log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, time.Since(start)) + }) +} + +// internal/middleware/recovery.go +func Recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("Panic recovered: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} +``` + +> **💡 老师的小揭秘:** +> * **链式调用**:中间件是 `http.Handler` 的装饰器,层层包裹。 +> * **Logger**:记录请求开始和结束时间,便于性能分析。 +> * **Recovery**:捕获 Panic,防止服务器崩溃,返回 500 错误。 + +--- + +## 6.7 主函数与优雅关闭 —— 餐厅的“打烊” + +> **💡 想象一下:** +> 打烊时,不能直接把顾客赶出去,要等他们吃完,再关灯关门。 +> 优雅关闭就是**等待当前请求完成**,再停止服务。 + +### 📝 主函数 + +```go +// cmd/server/main.go +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "task-api/internal/handler" + "task-api/internal/middleware" + "task-api/internal/repository" + "task-api/internal/service" + + "github.com/gorilla/mux" +) + +func main() { + // 初始化依赖 + taskRepo := repository.NewTaskRepo() + taskSvc := service.NewTaskService(taskRepo) + taskHandler := handler.NewTaskHandler(taskSvc) + + // 路由 + r := mux.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Recovery) + + r.HandleFunc("/tasks", taskHandler.CreateTask).Methods("POST") + r.HandleFunc("/tasks", taskHandler.ListTasks).Methods("GET") + r.HandleFunc("/tasks", taskHandler.GetTask).Methods("GET").Queries("id", "{id}") + + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + // 启动服务器 + go func() { + log.Println("Server starting on port 8080") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + }() + + // 优雅关闭 + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown:", err) + } + + log.Println("Server exited") +} +``` + +### 🔄 优雅关闭流程图 + +```mermaid +sequenceDiagram + participant OS + participant Main + participant Server + participant Req1 + participant Req2 + participant DB + + OS->>Main: SIGTERM + Main->>Server: Shutdown(ctx) + Server->>Req1: 拒绝新请求 + Server->>Req2: 拒绝新请求 + Req1-->>Server: 完成处理 + Req2-->>Server: 完成处理 + Server->>DB: 关闭连接 + Server-->>Main: 退出 + Main->>OS: 退出 +``` + +> **📖 深度解析:** +> 1. **收到信号**:OS 发送 `SIGTERM`。 +> 2. **停止监听**:`srv.Shutdown()` 停止接收新请求。 +> 3. **等待完成**:等待当前正在处理的请求(Req1, Req2)完成。 +> 4. **关闭资源**:关闭数据库连接、文件句柄等。 +> 5. **退出**:主进程退出。 +> 6. **超时控制**:`context.WithTimeout` 防止无限等待。 + +--- + +## 6.8 运行与测试 —— 开业大吉! + +> **🎉 现在,让我们启动餐厅!** + +```bash +# 1. 下载依赖 +go mod download + +# 2. 运行服务 +go run cmd/server/main.go + +# 3. 测试 API +# 创建任务 +curl -X POST http://localhost:8080/tasks \ + -H "Content-Type: application/json" \ + -d '{"title": "学习 Go", "description": "完成第六章", "priority": 5}' + +# 查询任务 +curl http://localhost:8080/tasks + +# 查询单个任务 +curl "http://localhost:8080/tasks?id=1" +``` + +> **💡 老师的小提醒:** +> * 如果看到 `Server starting on port 8080`,说明启动成功! +> * 用 `curl` 或 Postman 测试 API。 +> * 按 `Ctrl+C` 触发优雅关闭,观察日志。 + +--- + +## 6.9 本章小结 + +> **🎯 我们学到了什么?** +> 1. **分层架构**:Handler -> Service -> Repository,职责清晰。 +> 2. **数据模型**:自定义类型、JSON 标签。 +> 3. **并发安全**:用 Mutex 保护共享数据。 +> 4. **中间件**:日志、恢复,横切关注点。 +> 5. **优雅关闭**:信号处理、超时控制。 +> 6. **Docker 部署**:容器化,环境一致性。 +> +> **🚀 恭喜你!** +> 你已经完成了**从零基础到实战**的全过程!现在,你不仅掌握了 Go 语言的核心技能,还具备了**独立开发 Web 服务**的能力! +> +> **下一步**: +> * 尝试添加**数据库支持**(PostgreSQL/MySQL)。 +> * 添加**用户认证**(JWT)。 +> * 添加**缓存**(Redis)。 +> * 添加**监控**(Prometheus)。 +> +> **Go 的世界很大,你的旅程才刚刚开始!加油!** 🚀✨