Add Chapter 6: Web API Project - Full Stack Go Service with Middleware, DB, and Docker
This commit is contained in:
738
chapters/chapter-6-web-api.md
Normal file
738
chapters/chapter-6-web-api.md
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
# 第六章:实战项目 —— 构建一个简易 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(实战项目版)*
|
||||||
3
chapters/chapter-6/go.mod
Normal file
3
chapters/chapter-6/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module task-api
|
||||||
|
|
||||||
|
go 1.21
|
||||||
132
chapters/chapter-6/main.go
Normal file
132
chapters/chapter-6/main.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 模型
|
||||||
|
type Task struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内存存储(模拟数据库)
|
||||||
|
var tasks = make(map[int64]*Task)
|
||||||
|
var nextID int64 = 1
|
||||||
|
|
||||||
|
// Handler
|
||||||
|
func createTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var task Task
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Title == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "title is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.ID = nextID
|
||||||
|
nextID++
|
||||||
|
task.Status = "todo"
|
||||||
|
task.CreatedAt = time.Now()
|
||||||
|
task.UpdatedAt = time.Now()
|
||||||
|
tasks[task.ID] = &task
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusCreated, "created", task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
taskList := make([]*Task, 0, len(tasks))
|
||||||
|
for _, t := range tasks {
|
||||||
|
taskList = append(taskList, t)
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, "success", taskList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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 respondError(w http.ResponseWriter, code int, msg string) {
|
||||||
|
respondJSON(w, code, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
func loggerMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
log.Printf("Started %s %s", r.Method, r.URL.Path)
|
||||||
|
next(w, r)
|
||||||
|
log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, time.Since(start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return 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(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 路由
|
||||||
|
http.HandleFunc("/tasks", recoveryMiddleware(loggerMiddleware(createTask)))
|
||||||
|
http.HandleFunc("/tasks", recoveryMiddleware(loggerMiddleware(getTasks)))
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: nil, // 使用默认 ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user