20 KiB
20 KiB
第六章:实战项目 —— 构建一个简易 Web API 服务(深度图解版)
本章目标:综合运用前五章知识,构建一个生产级别的 Go Web API 服务(任务管理系统),涵盖路由、中间件、数据库、并发、配置、错误处理、部署等核心技能。 项目功能:
- 创建、读取、更新、删除(CRUD)任务(Task)。
- 支持任务状态(待办、进行中、已完成)。
- 支持按状态、优先级筛选。
- 异步处理任务通知(模拟)。
- 优雅关闭、健康检查、监控指标。
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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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 深度图解:请求处理流程
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: 记录结束时间
📖 深度解析:
- Client:发起 HTTP 请求。
- Router:匹配路由,调用中间件链。
- Logger:记录请求开始时间。
- Auth:验证 Token,失败直接返回 401。
- Recovery:包裹整个处理链,捕获 Panic。
- Handler:解析请求,调用 Service。
- Service:业务逻辑,调用 Repo。
- Repo:执行 SQL,映射结果。
- DB:执行数据库操作。
- 返回:沿调用链返回,Logger 记录结束时间。
6.4 深度图解:数据库连接池
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 深度图解:优雅关闭流程
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: 退出
📖 深度解析:
- 收到信号:OS 发送
SIGTERM。 - 停止监听:
srv.Shutdown()停止接收新请求。 - 等待完成:等待当前正在处理的请求(Req1, Req2)完成。
- 关闭资源:关闭数据库连接、文件句柄等。
- 退出:主进程退出。
- 超时控制:
context.WithTimeout防止无限等待。
6.6 Docker 部署
6.6.1 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
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 核心知识点回顾
- 分层架构:Handler -> Service -> Repository,职责清晰。
- 中间件:日志、认证、恢复,横切关注点。
- Context:超时控制、取消信号、优雅关闭。
- 连接池:数据库连接复用,性能优化。
- Docker:容器化部署,环境一致性。
6.7.2 扩展方向
- ORM:使用
gorm或sqlx简化数据库操作。 - 缓存:引入 Redis,缓存热点数据。
- 消息队列:使用 Kafka/RabbitMQ 处理异步任务。
- 监控:集成 Prometheus + Grafana。
- 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(实战项目版)