Files
test/chapters/chapter-6-web-api.md

739 lines
20 KiB
Markdown
Raw 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 Web API 服务(任务管理系统),涵盖路由、中间件、数据库、并发、配置、错误处理、部署等核心技能。
> **项目功能**
> 1. 创建、读取、更新、删除CRUD任务Task
> 2. 支持任务状态(待办、进行中、已完成)。
> 3. 支持按状态、优先级筛选。
> 4. 异步处理任务通知(模拟)。
> 5. 优雅关闭、健康检查、监控指标。
---
## 6.1 项目架构设计
### 6.1.1 目录结构
```
task-api/
├── cmd/
│ └── server/
│ └── main.go # 入口文件
├── internal/
│ ├── handler/ # HTTP 处理层Handler
│ │ └── task_handler.go
│ ├── service/ # 业务逻辑层Service
│ │ └── task_service.go
│ ├── repository/ # 数据访问层Repository
│ │ └── task_repo.go
│ ├── middleware/ # 中间件
│ │ ├── logger.go
│ │ ├── auth.go
│ │ └── recovery.go
│ ├── config/ # 配置管理
│ │ └── config.go
│ └── model/ # 数据模型
│ └── task.go
├── pkg/
│ └── logger/ # 公共工具
│ └── logger.go
├── migrations/ # 数据库迁移
│ └── 001_init.sql
├── .env # 环境变量
├── Dockerfile
├── docker-compose.yml
└── go.mod
```
**📖 深度解析**
- **cmd/**:应用入口,负责初始化配置、数据库连接、启动服务器。
- **internal/**:私有代码,外部无法导入,保证封装性。
- **handler**:处理 HTTP 请求/响应,参数校验,调用 Service。
- **service**:核心业务逻辑,调用 Repository处理事务。
- **repository**数据库操作SQL 查询,映射到 Model。
- **middleware**:横切关注点(日志、认证、限流)。
- **pkg/**:公共工具库,可被外部导入。
- **migrations/**:数据库结构变更脚本。
---
## 6.2 核心代码实现
### 6.2.1 数据模型 (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**:使用自定义类型(基于 string增强类型安全避免魔法字符串。
- **JSON 标签**:明确字段映射,支持 `omitempty` 等选项。
- **时间字段**`time.Time` 自动映射为 ISO 8601 格式。
---
### 6.2.2 配置管理 (Config)
```go
// internal/config/config.go
package config
import (
"os"
"strconv"
)
type Config struct {
Port string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
LogLevel string
}
func Load() *Config {
return &Config{
Port: getEnv("PORT", "8080"),
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "password"),
DBName: getEnv("DB_NAME", "taskdb"),
LogLevel: getEnv("LOG_LEVEL", "info"),
}
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
```
**📖 深度解析**
- **环境变量**:优先从环境变量读取,支持容器化部署。
- **默认值**:提供安全默认值,避免配置缺失导致崩溃。
- **集中管理**:所有配置集中加载,便于传递和测试。
---
### 6.2.3 数据库连接池 (Repository)
```go
// internal/repository/task_repo.go
package repository
import (
"context"
"database/sql"
"fmt"
"task-api/internal/model"
)
type TaskRepo struct {
db *sql.DB
}
func NewTaskRepo(db *sql.DB) *TaskRepo {
return &TaskRepo{db: db}
}
// 创建任务
func (r *TaskRepo) Create(ctx context.Context, task *model.Task) error {
query := `INSERT INTO tasks (title, description, status, priority, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW()) RETURNING id`
err := r.db.QueryRowContext(ctx, query,
task.Title, task.Description, task.Status, task.Priority).Scan(&task.ID)
if err != nil {
return fmt.Errorf("create task failed: %w", err)
}
return nil
}
// 获取所有任务
func (r *TaskRepo) List(ctx context.Context, status *model.Status) ([]model.Task, error) {
query := `SELECT id, title, description, status, priority, created_at, updated_at FROM tasks`
if status != nil {
query += " WHERE status = $1"
rows, err := r.db.QueryContext(ctx, query, *status)
if err != nil {
return nil, err
}
defer rows.Close()
// ... 解析 rows
} else {
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
// ... 解析 rows
}
return tasks, nil
}
// 更新任务
func (r *TaskRepo) Update(ctx context.Context, task *model.Task) error {
query := `UPDATE tasks SET title=$1, description=$2, status=$3, priority=$4, updated_at=NOW()
WHERE id=$5`
res, err := r.db.ExecContext(ctx, query,
task.Title, task.Description, task.Status, task.Priority, task.ID)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows == 0 {
return fmt.Errorf("task not found")
}
return nil
}
// 删除任务
func (r *TaskRepo) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM tasks WHERE id = $1`
res, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows == 0 {
return fmt.Errorf("task not found")
}
return nil
}
```
**📖 深度解析**
- **Context 传递**:所有 DB 操作都接收 `context.Context`,支持超时控制和取消。
- **预编译语句**:使用 `$1, $2` 防止 SQL 注入。
- **连接池**`sql.DB` 内部维护连接池,自动管理连接生命周期。
- **错误包装**:使用 `fmt.Errorf("%w")` 包装错误,保留原始错误信息。
- **资源管理**`defer rows.Close()` 确保资源释放。
---
### 6.2.4 业务逻辑层 (Service)
```go
// internal/service/task_service.go
package service
import (
"context"
"errors"
"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) {
// 模拟从 repo 获取
task, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, ErrNotFound
}
return task, nil
}
// 异步通知示例
func (s *TaskService) NotifyTask(ctx context.Context, taskID int64) {
go func() {
// 模拟发送通知
// 注意:这里没有接收 context实际项目中应传递 context 并处理超时
// 使用 context.WithTimeout 创建子 context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 发送通知逻辑...
select {
case <-ctx.Done():
// 超时
default:
// 成功
}
}()
}
```
**📖 深度解析**
- **参数校验**:在 Service 层进行业务规则校验。
- **错误定义**:定义自定义错误(`ErrNotFound`),便于 Handler 层统一处理。
- **异步任务**:使用 `go func()` 启动 Goroutine注意处理 Context 和超时。
- **依赖注入**Service 依赖 Repository便于测试和替换。
---
### 6.2.5 HTTP 处理层 (Handler)
```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) 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()` 自动包含超时和取消信号。
- **JSON 编解码**:使用标准库 `encoding/json`
---
### 6.2.6 中间件 (Middleware)
```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)
})
}
// internal/middleware/auth.go
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "secret-token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
```
**📖 深度解析**
- **链式调用**:中间件是 `http.Handler` 的装饰器,层层包裹。
- **Logger**:记录请求开始和结束时间,便于性能分析。
- **Recovery**:捕获 Panic防止服务器崩溃返回 500 错误。
- **Auth**:简单的 Token 验证,实际项目应使用 JWT。
---
### 6.2.7 主函数与优雅关闭 (Main)
```go
// cmd/server/main.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"task-api/internal/config"
"task-api/internal/handler"
"task-api/internal/middleware"
"task-api/internal/repository"
"task-api/internal/service"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"database/sql"
)
func main() {
cfg := config.Load()
// 连接数据库
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName)
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 初始化依赖
taskRepo := repository.NewTaskRepo(db)
taskSvc := service.NewTaskService(taskRepo)
taskHandler := handler.NewTaskHandler(taskSvc)
// 路由
r := mux.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recovery)
r.Use(middleware.Auth)
r.HandleFunc("/tasks", taskHandler.CreateTask).Methods("POST")
r.HandleFunc("/tasks", taskHandler.GetTask).Methods("GET")
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
}
// 启动服务器
go func() {
log.Printf("Server starting on port %s", cfg.Port)
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")
}
```
**📖 深度解析**
- **依赖注入**:从 Config -> Repo -> Service -> Handler层层构建。
- **中间件链**`r.Use()` 按顺序添加中间件Logger -> Recovery -> Auth
- **优雅关闭**
- 监听 `SIGINT` (Ctrl+C) 和 `SIGTERM` (Docker 停止) 信号。
- 收到信号后,调用 `srv.Shutdown(ctx)`,等待当前请求完成(最多 5 秒)。
- 超时后强制关闭。
- **并发启动**`go func()` 启动服务器,主进程等待信号。
---
## 6.3 深度图解:请求处理流程
```mermaid
sequenceDiagram
participant Client
participant Router
participant Logger
participant Auth
participant Recovery
participant Handler
participant Service
participant Repo
participant DB
Client->>Router: POST /tasks
Router->>Logger: 调用 Logger 中间件
Logger->>Auth: 记录开始时间
Auth->>Recovery: 验证 Token
Recovery->>Handler: 捕获 Panic
Handler->>Service: CreateTask
Service->>Repo: Create
Repo->>DB: INSERT INTO tasks
DB-->>Repo: 返回 ID
Repo-->>Service: Task with ID
Service-->>Handler: nil
Handler-->>Client: 201 Created + JSON
Logger->>Client: 记录结束时间
```
**📖 深度解析**
1. **Client**:发起 HTTP 请求。
2. **Router**:匹配路由,调用中间件链。
3. **Logger**:记录请求开始时间。
4. **Auth**:验证 Token失败直接返回 401。
5. **Recovery**:包裹整个处理链,捕获 Panic。
6. **Handler**:解析请求,调用 Service。
7. **Service**:业务逻辑,调用 Repo。
8. **Repo**:执行 SQL映射结果。
9. **DB**:执行数据库操作。
10. **返回**沿调用链返回Logger 记录结束时间。
---
## 6.4 深度图解:数据库连接池
```mermaid
graph TB
subgraph "应用层"
App[Go 应用]
Pool[sql.DB 连接池]
end
subgraph "数据库层"
DB[(PostgreSQL)]
Conn1[连接 1]
Conn2[连接 2]
Conn3[连接 3]
end
App <--> Pool
Pool <--> Conn1
Pool <--> Conn2
Pool <--> Conn3
Note over Pool: 最大连接数20<br/>空闲超时30s<br/>最大生命周期1h
style Pool fill:#ffeb3b,stroke:#333
style DB fill:#2196f3,stroke:#333,color:#fff
```
**📖 深度解析**
- **sql.DB**:不是单个连接,而是**连接池**。
- **最大连接数**:通过 `db.SetMaxOpenConns(n)` 设置,避免耗尽数据库资源。
- **空闲超时**`db.SetConnMaxLifetime()` 设置连接最大生命周期。
- **复用**:连接用完归还池,下次请求直接复用,避免频繁创建/销毁。
---
## 6.5 深度图解:优雅关闭流程
```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. **退出**:主进程退出。
- **超时控制**`context.WithTimeout` 防止无限等待。
---
## 6.6 Docker 部署
### 6.6.1 Dockerfile
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/server
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
```
### 6.6.2 docker-compose.yml
```yaml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=password
- DB_NAME=taskdb
depends_on:
- db
restart: always
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: taskdb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres_data:
```
**📖 深度解析**
- **多阶段构建**:第一阶段编译,第二阶段只复制二进制文件,镜像更小。
- **环境变量**:通过 `docker-compose` 传递配置。
- **依赖管理**`depends_on` 确保数据库先启动。
- **数据持久化**Volume 挂载,防止数据丢失。
---
## 6.7 总结与扩展
### 6.7.1 核心知识点回顾
1. **分层架构**Handler -> Service -> Repository职责清晰。
2. **中间件**:日志、认证、恢复,横切关注点。
3. **Context**:超时控制、取消信号、优雅关闭。
4. **连接池**:数据库连接复用,性能优化。
5. **Docker**:容器化部署,环境一致性。
### 6.7.2 扩展方向
1. **ORM**:使用 `gorm``sqlx` 简化数据库操作。
2. **缓存**:引入 Redis缓存热点数据。
3. **消息队列**:使用 Kafka/RabbitMQ 处理异步任务。
4. **监控**:集成 Prometheus + Grafana。
5. **CI/CD**:自动化测试、构建、部署。
---
**代码仓库位置**https://giter.top/openclaw/test/tree/main/chapters/chapter-6
**完整项目代码**https://giter.top/openclaw/test/tree/main/chapters/chapter-6
---
*最后更新2026-03-24 00:05 UTC实战项目版*