Skip to content

第12章 实战项目:迷你 Web 服务(Todo API)

📚 学习目标:把前 11 章的知识串起来,做一个可运行、能扩展的 Todo 服务。


🎯 项目目标

写一个 Todo API,对外提供 RESTful 接口:

方法路径功能
GET/todos列出所有 todo(支持分页、筛选)
POST/todos创建一个 todo
GET/todos/{id}获取单个 todo
PUT/todos/{id}更新 todo
DELETE/todos/{id}删除 todo
POST/todos/{id}/done标记完成

附带:

  • 中间件:日志、CORS、panic recovery
  • 优雅关闭
  • 配置文件
  • 单元测试

🏗 项目结构

miniweb/
├── cmd/
│   └── server/
│       └── main.go              # 入口
├── internal/
│   ├── handler/                 # HTTP handler 层
│   │   ├── todo.go
│   │   └── todo_test.go
│   ├── service/                 # 业务逻辑
│   │   └── todo.go
│   ├── repository/              # 数据存储
│   │   └── memory.go
│   ├── model/
│   │   └── todo.go
│   └── middleware/
│       ├── logger.go
│       ├── cors.go
│       └── recovery.go
├── go.mod
└── README.md

这是 Go 工程通行的 三层架构:handler → service → repository。


1️⃣ Model(数据模型)

go
// internal/model/todo.go
package model

import "time"

type Todo struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Done      bool      `json:"done"`
    CreatedAt time.Time `json:"created_at"`
}

2️⃣ Repository(存储层)

最简单的实现:内存存储。后续可以替换成 MySQL、Redis、etcd…

go
// internal/repository/memory.go
type MemoryRepo struct {
    mu     sync.RWMutex
    data   map[int]*model.Todo
    nextID int
}

func (r *MemoryRepo) Create(t *model.Todo) (*model.Todo, error) { ... }
func (r *MemoryRepo) Get(id int) (*model.Todo, error)           { ... }
func (r *MemoryRepo) List() []*model.Todo                        { ... }
func (r *MemoryRepo) Update(t *model.Todo) error                 { ... }
func (r *MemoryRepo) Delete(id int) error                        { ... }

关键:所有方法都用 sync.RWMutex 保护——这是并发安全的基础。


3️⃣ Service(业务层)

业务规则集中处理。比如"标题不能为空"、"已完成的不能再次完成"。

go
type TodoService struct {
    repo Repository
}

func (s *TodoService) Create(title string) (*model.Todo, error) {
    title = strings.TrimSpace(title)
    if title == "" {
        return nil, ErrTitleEmpty
    }
    return s.repo.Create(&model.Todo{
        Title:     title,
        CreatedAt: time.Now(),
    })
}

service 层只依赖 Repository 接口,不依赖具体实现。这样换 DB 时不用动业务代码。


4️⃣ Handler(HTTP 层)

go
type TodoHandler struct {
    svc *service.TodoService
}

func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req struct{ Title string `json:"title"` }
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeJSON(w, 400, map[string]string{"error": "invalid json"})
        return
    }
    t, err := h.svc.Create(req.Title)
    if err != nil {
        writeJSON(w, 400, map[string]string{"error": err.Error()})
        return
    }
    writeJSON(w, 201, t)
}

5️⃣ 中间件(洋葱模型)

5.1 logger

go
func Logger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

5.2 recovery(捕获 panic 不让进程崩)

go
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v", rec)
                http.Error(w, "internal server error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

5.3 CORS

go
func CORS(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
        if r.Method == "OPTIONS" {
            w.WriteHeader(204)
            return
        }
        next.ServeHTTP(w, r)
    })
}

6️⃣ main.go:组装一切

go
func main() {
    flag.Parse()

    repo := repository.NewMemoryRepo()
    svc := service.NewTodoService(repo)
    handler := handler.NewTodoHandler(svc)

    mux := http.NewServeMux()
    handler.Register(mux)

    chain := middleware.CORS(middleware.Logger(middleware.Recovery(mux)))

    srv := &http.Server{
        Addr:    ":" + *port,
        Handler: chain,
    }

    // 优雅关闭
    go func() {
        log.Printf("listening on %s", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    log.Println("shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
    log.Println("bye")
}

🚀 运行 & 测试

bash
cd 12-project/miniweb

# 1. 启动服务
go run ./cmd/server

# 2. 创建 todo
curl -X POST http://localhost:8080/todos \
     -H "Content-Type: application/json" \
     -d '{"title":"学 Go"}'
# → {"id":1,"title":"学 Go","done":false,"created_at":"..."}

# 3. 列出
curl http://localhost:8080/todos

# 4. 标完成
curl -X POST http://localhost:8080/todos/1/done

# 5. 删除
curl -X DELETE http://localhost:8080/todos/1

# 6. 跑测试
go test ./...

# 7. 优雅关闭:Ctrl+C

📊 架构图

┌─────────────────────────────────────────────────────────────┐
│  Client (curl / browser / mobile)                           │
└─────────────────────┬───────────────────────────────────────┘
                      │ HTTP
┌─────────────────────▼───────────────────────────────────────┐
│  cmd/server/main.go                                         │
│   ↓                                                         │
│  Middleware Chain: CORS → Logger → Recovery                 │
│   ↓                                                         │
│  internal/handler  (HTTP I/O,参数校验、状态码)             │
│   ↓                                                         │
│  internal/service  (业务规则、事务、组合)                   │
│   ↓                                                         │
│  internal/repository  (数据存储抽象)                        │
│   ↓                                                         │
│   ┌───────────┐  ┌───────────┐  ┌──────────┐               │
│   │ Memory    │  │ MySQL     │  │ Redis    │ ← 可替换      │
│   └───────────┘  └───────────┘  └──────────┘               │
└─────────────────────────────────────────────────────────────┘

🧠 设计原则总结

  1. 依赖倒置:service 依赖 Repository 接口而不是具体实现 → 换存储不影响业务
  2. 单一职责:handler 只管 HTTP 协议,service 只管业务,repo 只管存储
  3. 错误向上传:每层只处理自己关心的错误,剩下的 return err
  4. 并发安全:repo 用读写锁保护共享 map
  5. 优雅关闭:捕获 SIGTERM,给已有请求 10 秒善后
  6. 可观测性:日志、错误信息、请求耗时都打到 stdout
  7. 可测试:service 可单测(mock repo);handler 可用 httptest

📁 本章配套


🎓 通关恭喜!

读到这里,你已经完成了:

  1. Go 语法基础(变量、控制流、函数、集合、struct、接口、并发、错误处理)
  2. 工程能力(包管理、标准库、并发模型、HTTP 服务)
  3. 实战项目(三层架构、中间件、优雅关闭)

下一步建议

  • 把这个项目的 MemoryRepo 改成 MySQL 或 Postgres
  • 引入 ginchi 框架对比标准库
  • 学习微服务(gRPC + 服务注册)
  • 阅读优秀开源项目:dockerkubernetesprometheusetcdgincobraviper

🎉 Happy hacking with Go!