Files
test/chapters/chapter-6-revised.md

588 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第六章:实战项目 —— 构建一个 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 的世界很大,你的旅程才刚刚开始!加油!** 🚀✨