综合项目(下)
第30课:综合项目(下)
生活类比引入
上一课我们完成了"任务管理系统 TaskFlow"的需求分析、目录结构设计、数据模型和核心业务逻辑——就像盖房子打好了地基、立好了框架。这一课,我们要完成装修和交付:
- REST API = 给房子装上门窗,让外面的人能进来
- 中间件 = 门禁系统(认证)、监控摄像头(日志)、访客登记(CORS)
- 数据库集成 = 把临时家具换成定制家具,持久耐用
- 集成测试 = 验收检查,确保水电煤气都正常
- API 文档 = 使用说明书
- Dockerfile = 把房子打包成集装箱,搬到哪里都能住
学完这节课,你将拥有一个可部署、可测试、有文档的完整 Go 项目。
项目回顾
上一课我们搭建了 TaskFlow 的核心骨架,本课继续完善。如果你还没有阅读 第29课:综合项目(上),建议先完成上半部分。
最终项目结构:
taskflow/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── model/
│ │ ├── task.go # 数据模型
│ │ └── task_test.go # 模型测试
│ ├── store/
│ │ ├── store.go # 存储接口
│ │ ├── memory.go # 内存实现(上一课)
│ │ └── sqlite.go # SQLite实现(本课新增)
│ ├── service/
│ │ ├── task.go # 业务逻辑
│ │ └── task_test.go # 单元测试
│ ├── handler/
│ │ ├── task.go # HTTP处理器
│ │ └── task_test.go # 处理器测试
│ └── middleware/
│ ├── auth.go # 认证中间件
│ ├── logging.go # 日志中间件
│ └── cors.go # CORS中间件
├── docs/
│ └── api.md # API文档
├── Dockerfile # 容器化打包
├── docker-compose.yml # 编排配置
├── go.mod
├── go.sum
└── README.md
1. REST API层完善
1.1 路由设计
基于 RESTful 规范设计 API 路由:
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/tasks |
获取任务列表 |
GET |
/api/tasks/{id} |
获取单个任务 |
POST |
/api/tasks |
创建任务 |
PUT |
/api/tasks/{id} |
更新任务 |
DELETE |
/api/tasks/{id} |
删除任务 |
GET |
/health |
健康检查 |
1.2 完整处理器实现
// internal/handler/task.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"taskflow/internal/model"
"taskflow/internal/service"
)
// TaskHandler 任务HTTP处理器
type TaskHandler struct {
svc *service.TaskService
}
// NewTaskHandler 创建处理器实例
func NewTaskHandler(svc *service.TaskService) *TaskHandler {
return &TaskHandler{svc: svc}
}
// ErrorResponse 统一错误响应格式
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message,omitempty"`
}
// SuccessResponse 统一成功响应格式
type SuccessResponse struct {
Data interface{} `json:"data"`
Message string `json:"message,omitempty"`
}
// writeJSON 写入JSON响应
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// writeError 写入错误响应
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, ErrorResponse{
Error: msg,
Code: status,
})
}
// extractID 从URL路径中提取ID参数
func extractID(r *http.Request) (int, error) {
// 路径格式: /api/tasks/{id}
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) < 3 {
return 0, errors.New("缺少ID参数")
}
return strconv.Atoi(parts[2])
}
// ListTasks 处理 GET /api/tasks
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
// 解析查询参数
query := r.URL.Query()
filter := service.TaskFilter{
Status: model.TaskStatus(query.Get("status")),
}
// 解析分页参数
page, _ := strconv.Atoi(query.Get("page"))
pageSize, _ := strconv.Atoi(query.Get("page_size"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
tasks, total, err := h.svc.ListTasks(r.Context(), filter, page, pageSize)
if err != nil {
writeError(w, http.StatusInternalServerError, "查询失败: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"data": tasks,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// GetTask 处理 GET /api/tasks/{id}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "无效的任务ID")
return
}
task, err := h.svc.GetTask(r.Context(), id)
if err != nil {
if errors.Is(err, service.ErrTaskNotFound) {
writeError(w, http.StatusNotFound, "任务不存在")
return
}
writeError(w, http.StatusInternalServerError, "查询失败")
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Data: task})
}
// CreateTask 处理 POST /api/tasks
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误")
return
}
task, err := h.svc.CreateTask(r.Context(), &req)
if err != nil {
if errors.Is(err, service.ErrValidation) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeError(w, http.StatusInternalServerError, "创建失败")
return
}
writeJSON(w, http.StatusCreated, SuccessResponse{
Data: task,
Message: "任务创建成功",
})
}
// UpdateTask 处理 PUT /api/tasks/{id}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "无效的任务ID")
return
}
var req model.UpdateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误")
return
}
task, err := h.svc.UpdateTask(r.Context(), id, &req)
if err != nil {
switch {
case errors.Is(err, service.ErrTaskNotFound):
writeError(w, http.StatusNotFound, "任务不存在")
case errors.Is(err, service.ErrValidation):
writeError(w, http.StatusBadRequest, err.Error())
default:
writeError(w, http.StatusInternalServerError, "更新失败")
}
return
}
writeJSON(w, http.StatusOK, SuccessResponse{
Data: task,
Message: "任务更新成功",
})
}
// DeleteTask 处理 DELETE /api/tasks/{id}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "无效的任务ID")
return
}
if err := h.svc.DeleteTask(r.Context(), id); err != nil {
if errors.Is(err, service.ErrTaskNotFound) {
writeError(w, http.StatusNotFound, "任务不存在")
return
}
writeError(w, http.StatusInternalServerError, "删除失败")
return
}
writeJSON(w, http.StatusOK, SuccessResponse{Message: "任务删除成功"})
}
// HealthCheck 健康检查端点
func (h *TaskHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"status": "ok",
"service": "taskflow",
})
}
1.3 路由注册与服务器启动
// cmd/server/main.go
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"taskflow/internal/handler"
"taskflow/internal/middleware"
"taskflow/internal/service"
"taskflow/internal/store"
)
func main() {
// 初始化日志
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
// 初始化存储层(默认使用SQLite)
dbPath := getEnv("DB_PATH", "taskflow.db")
st, err := store.NewSQLiteStore(dbPath)
if err != nil {
slog.Error("数据库初始化失败", "error", err)
os.Exit(1)
}
defer st.Close()
// 初始化业务层
svc := service.NewTaskService(st)
// 初始化处理器
h := handler.NewTaskHandler(svc)
// 注册路由
mux := http.NewServeMux()
// 健康检查
mux.HandleFunc("GET /health", h.HealthCheck)
// API路由
mux.HandleFunc("GET /api/tasks", h.ListTasks)
mux.HandleFunc("POST /api/tasks", h.CreateTask)
mux.HandleFunc("GET /api/tasks/{id}", h.GetTask)
mux.HandleFunc("PUT /api/tasks/{id}", h.UpdateTask)
mux.HandleFunc("DELETE /api/tasks/{id}", h.DeleteTask)
// 组装中间件链:CORS → 日志 → 认证 → 业务处理
apiKey := getEnv("API_KEY", "dev-secret-key")
handlerChain := middleware.Chain(
mux,
middleware.CORS(),
middleware.Logging(),
middleware.Auth(apiKey),
)
// 创建HTTP服务器
addr := ":" + getEnv("PORT", "8080")
srv := &http.Server{
Addr: addr,
Handler: handlerChain,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 启动服务器(非阻塞)
go func() {
slog.Info("TaskFlow服务启动", "addr", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("服务器异常退出", "error", err)
os.Exit(1)
}
}()
// 优雅关闭:监听系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("收到关闭信号,正在优雅关闭...")
// 给正在处理的请求5秒时间完成
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("服务器关闭异常", "error", err)
}
slog.Info("TaskFlow服务已停止")
}
// getEnv 获取环境变量,支持默认值
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
net/http 支持方法匹配("GET /path")和路径参数({id}),无需第三方路由库即可构建 RESTful API。
2. 中间件集成
2.1 中间件链模式
中间件是 HTTP 请求处理管道中的"拦截器",可以在请求到达处理器之前和之后执行通用逻辑:
请求 → [CORS] → [日志] → [认证] → [处理器] → 响应
↓ ↓ ↓
跨域控制 记录耗时 验证身份
2.2 中间件基础设施
// internal/middleware/middleware.go
package middleware
import "net/http"
// Middleware 中间件类型定义
type Middleware func(http.Handler) http.Handler
// Chain 将多个中间件串联成链,最先传入的最先执行
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
// 从后往前包装,确保执行顺序是从左到右
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
2.3 CORS中间件
// internal/middleware/cors.go
package middleware
import "net/http"
// CORS 处理跨域资源共享
func CORS() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置CORS响应头
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, X-Request-ID")
w.Header().Set("Access-Control-Max-Age", "86400") // 预检缓存24小时
// 预检请求直接返回
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
2.4 日志中间件
// internal/middleware/logging.go
package middleware
import (
"log/slog"
"net/http"
"time"
)
// responseWriter 包装ResponseWriter以捕获状态码
type responseWriter struct {
http.ResponseWriter
statusCode int
written int64
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.written += int64(n)
return n, err
}
// Logging 记录每个请求的处理日志
func Logging() Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装ResponseWriter以捕获状态码
rw := newResponseWriter(w)
// 调用下一个处理器
next.ServeHTTP(rw, r)
// 记录请求日志
duration := time.Since(start)
slog.Info("HTTP请求",
"method", r.Method,
"path", r.URL.Path,
"status", rw.statusCode,
"duration_ms", duration.Milliseconds(),
"remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}
}
2.5 认证中间件
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
)
// Auth 验证API密钥
func Auth(apiKey string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 健康检查端点无需认证
if r.URL.Path == "/health" {
next.ServeHTTP(w, r)
return
}
// 从请求头提取Token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.Header().Set("WWW-Authenticate", `Bearer realm="taskflow"`)
http.Error(w, `{"error":"缺少认证信息","code":401}`, http.StatusUnauthorized)
return
}
// 验证Bearer Token格式
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, `{"error":"认证格式错误,应为 Bearer <token>","code":401}`, http.StatusUnauthorized)
return
}
// 验证Token值
if parts[1] != apiKey {
http.Error(w, `{"error":"认证失败:无效的API密钥","code":403}`, http.StatusForbidden)
return
}
// 认证通过,继续处理
next.ServeHTTP(w, r)
})
}
}
3. 数据库集成(SQLite)
3.1 为什么选择SQLite
| 特性 | SQLite | MySQL/PostgreSQL |
|---|---|---|
| 安装 | 零配置,单文件 | 需要独立服务 |
| 适用场景 | 中小项目、嵌入式 | 大型生产环境 |
| 并发 | 读并发,写串行 | 高并发 |
| 迁移 | 需自行实现 | 工具丰富 |
3.2 SQLite存储实现
// internal/store/sqlite.go
package store
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3" // SQLite驱动,匿名导入触发初始化
"taskflow/internal/model"
)
// SQLiteStore SQLite存储实现
type SQLiteStore struct {
db *sql.DB
}
// NewSQLiteStore 创建并初始化SQLite存储
func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("打开数据库失败: %w", err)
}
// 验证连接
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("数据库连接失败: %w", err)
}
// 配置连接池
db.SetMaxOpenConns(1) // SQLite单写,限制并发写入
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) // 连接不过期
// 执行建表迁移
if err := migrate(db); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
return &SQLiteStore{db: db}, nil
}
// migrate 执行数据库迁移
func migrate(db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
priority TEXT NOT NULL DEFAULT 'medium',
due_date DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date);
`
_, err := db.Exec(query)
return err
}
// Close 关闭数据库连接
func (s *SQLiteStore) Close() error {
return s.db.Close()
}
// Create 创建任务
func (s *SQLiteStore) Create(ctx context.Context, task *model.Task) error {
query := `
INSERT INTO tasks (title, description, status, priority, due_date, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
now := time.Now()
result, err := s.db.ExecContext(ctx, query,
task.Title, task.Description, task.Status, task.Priority,
task.DueDate, now, now,
)
if err != nil {
return fmt.Errorf("插入任务失败: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("获取插入ID失败: %w", err)
}
task.ID = int(id)
task.CreatedAt = now
task.UpdatedAt = now
return nil
}
// GetByID 根据ID查询任务
func (s *SQLiteStore) GetByID(ctx context.Context, id int) (*model.Task, error) {
query := `
SELECT id, title, description, status, priority, due_date, created_at, updated_at
FROM tasks WHERE id = ?
`
var task model.Task
var dueDate sql.NullTime
err := s.db.QueryRowContext(ctx, query, id).Scan(
&task.ID, &task.Title, &task.Description,
&task.Status, &task.Priority, &dueDate,
&task.CreatedAt, &task.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil // 未找到返回nil
}
if err != nil {
return nil, fmt.Errorf("查询任务失败: %w", err)
}
if dueDate.Valid {
task.DueDate = &dueDate.Time
}
return &task, nil
}
// List 查询任务列表(支持过滤和分页)
func (s *SQLiteStore) List(ctx context.Context, filter TaskFilter, page, pageSize int) ([]model.Task, int, error) {
// 构建查询条件
where := "1=1"
args := []interface{}{}
if filter.Status != "" {
where += " AND status = ?"
args = append(args, filter.Status)
}
if filter.Priority != "" {
where += " AND priority = ?"
args = append(args, filter.Priority)
}
// 查询总数
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM tasks WHERE %s", where)
var total int
if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("查询总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
query := fmt.Sprintf(`
SELECT id, title, description, status, priority, due_date, created_at, updated_at
FROM tasks WHERE %s
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, where)
args = append(args, pageSize, offset)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, 0, fmt.Errorf("查询列表失败: %w", err)
}
defer rows.Close()
var tasks []model.Task
for rows.Next() {
var task model.Task
var dueDate sql.NullTime
if err := rows.Scan(
&task.ID, &task.Title, &task.Description,
&task.Status, &task.Priority, &dueDate,
&task.CreatedAt, &task.UpdatedAt,
); err != nil {
return nil, 0, fmt.Errorf("扫描行数据失败: %w", err)
}
if dueDate.Valid {
task.DueDate = &dueDate.Time
}
tasks = append(tasks, task)
}
return tasks, total, nil
}
// Update 更新任务
func (s *SQLiteStore) Update(ctx context.Context, task *model.Task) error {
query := `
UPDATE tasks
SET title = ?, description = ?, status = ?, priority = ?, due_date = ?, updated_at = ?
WHERE id = ?
`
result, err := s.db.ExecContext(ctx, query,
task.Title, task.Description, task.Status, task.Priority,
task.DueDate, time.Now(), task.ID,
)
if err != nil {
return fmt.Errorf("更新任务失败: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("任务不存在")
}
return nil
}
// Delete 删除任务
func (s *SQLiteStore) Delete(ctx context.Context, id int) error {
query := "DELETE FROM tasks WHERE id = ?"
result, err := s.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("删除任务失败: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("任务不存在")
}
return nil
}
// TaskFilter 任务过滤条件
type TaskFilter struct {
Status model.TaskStatus
Priority model.TaskPriority
}
3.3 存储接口统一
// internal/store/store.go
package store
import (
"context"
"taskflow/internal/model"
)
// TaskStore 定义任务存储接口
// 所有存储实现(内存、SQLite、PostgreSQL)都必须实现此接口
type TaskStore interface {
Create(ctx context.Context, task *model.Task) error
GetByID(ctx context.Context, id int) (*model.Task, error)
List(ctx context.Context, filter TaskFilter, page, pageSize int) ([]model.Task, int, error)
Update(ctx context.Context, task *model.Task) error
Delete(ctx context.Context, id int) error
Close() error
}
TaskStore 接口,不关心底层是 SQLite 还是内存。测试时注入 mock,生产时注入真实实现。
4. 集成测试
4.1 处理器集成测试
集成测试验证多个组件协作是否正常,使用 httptest 模拟真实HTTP请求:
// internal/handler/task_test.go
package handler_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"taskflow/internal/handler"
"taskflow/internal/middleware"
"taskflow/internal/model"
"taskflow/internal/service"
"taskflow/internal/store"
)
// setupTestServer 创建测试服务器
func setupTestServer(t *testing.T) *httptest.Server {
t.Helper()
// 使用内存存储
memStore := store.NewMemoryStore()
svc := service.NewTaskService(memStore)
h := handler.NewTaskHandler(svc)
// 注册路由
mux := http.NewServeMux()
mux.HandleFunc("GET /health", h.HealthCheck)
mux.HandleFunc("GET /api/tasks", h.ListTasks)
mux.HandleFunc("POST /api/tasks", h.CreateTask)
mux.HandleFunc("GET /api/tasks/{id}", h.GetTask)
mux.HandleFunc("PUT /api/tasks/{id}", h.UpdateTask)
mux.HandleFunc("DELETE /api/tasks/{id}", h.DeleteTask)
// 添加中间件
apiKey := "test-key"
handlerChain := middleware.Chain(
mux,
middleware.CORS(),
middleware.Logging(),
middleware.Auth(apiKey),
)
return httptest.NewServer(handlerChain)
}
// TestIntegration_CRUD 完整CRUD流程集成测试
func TestIntegration_CRUD(t *testing.T) {
srv := setupTestServer(t)
defer srv.Close()
client := srv.Client()
authHeader := "Bearer test-key"
// 1. 健康检查
t.Run("健康检查", func(t *testing.T) {
resp, err := client.Get(srv.URL + "/health")
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("状态码 = %d, 期望 200", resp.StatusCode)
}
})
// 2. 创建任务
var createdTask model.Task
t.Run("创建任务", func(t *testing.T) {
body := `{"title":"集成测试任务","description":"测试CRUD流程","priority":"high"}`
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/tasks", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", authHeader)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Errorf("状态码 = %d, 期望 201", resp.StatusCode)
}
var result struct {
Data model.Task `json:"data"`
Message string `json:"message"`
}
json.NewDecoder(resp.Body).Decode(&result)
createdTask = result.Data
if createdTask.Title != "集成测试任务" {
t.Errorf("Title = %q, 期望 %q", createdTask.Title, "集成测试任务")
}
})
// 3. 查询单个任务
t.Run("查询任务", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks/1", nil)
req.Header.Set("Authorization", authHeader)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("状态码 = %d, 期望 200", resp.StatusCode)
}
})
// 4. 更新任务
t.Run("更新任务", func(t *testing.T) {
body := `{"title":"已更新的任务","status":"completed"}`
req, _ := http.NewRequest(http.MethodPut, srv.URL+"/api/tasks/1", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", authHeader)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("状态码 = %d, 期望 200", resp.StatusCode)
}
})
// 5. 查询列表
t.Run("查询列表", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks?page=1&page_size=10", nil)
req.Header.Set("Authorization", authHeader)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("状态码 = %d, 期望 200", resp.StatusCode)
}
var result struct {
Data []model.Task `json:"data"`
Total int `json:"total"`
}
json.NewDecoder(resp.Body).Decode(&result)
if result.Total != 1 {
t.Errorf("Total = %d, 期望 1", result.Total)
}
})
// 6. 删除任务
t.Run("删除任务", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/tasks/1", nil)
req.Header.Set("Authorization", authHeader)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("状态码 = %d, 期望 200", resp.StatusCode)
}
})
// 7. 验证删除后查询返回404
t.Run("删除后查询应返回404", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks/1", nil)
req.Header.Set("Authorization", authHeader)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("状态码 = %d, 期望 404", resp.StatusCode)
}
})
}
// TestIntegration_Auth 认证中间件集成测试
func TestIntegration_Auth(t *testing.T) {
srv := setupTestServer(t)
defer srv.Close()
client := srv.Client()
tests := []struct {
name string
authHeader string
wantCode int
}{
{"无认证头", "", http.StatusUnauthorized},
{"格式错误", "Basic abc123", http.StatusUnauthorized},
{"错误密钥", "Bearer wrong-key", http.StatusForbidden},
{"正确认证", "Bearer test-key", http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks", nil)
if tt.authHeader != "" {
req.Header.Set("Authorization", tt.authHeader)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != tt.wantCode {
t.Errorf("状态码 = %d, 期望 %d", resp.StatusCode, tt.wantCode)
}
})
}
}
// TestIntegration_CORS 跨域测试
func TestIntegration_CORS(t *testing.T) {
srv := setupTestServer(t)
defer srv.Close()
client := srv.Client()
// OPTIONS预检请求
req, _ := http.NewRequest(http.MethodOptions, srv.URL+"/api/tasks", nil)
req.Header.Set("Origin", "http://example.com")
req.Header.Set("Access-Control-Request-Method", "POST")
resp, err := client.Do(req)
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
t.Errorf("状态码 = %d, 期望 204", resp.StatusCode)
}
if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
t.Error("缺少CORS响应头")
}
}
4.2 运行集成测试
# 运行所有测试
go test ./... -v
# 只运行集成测试
go test -run TestIntegration -v
# 查看覆盖率
go test ./... -cover -coverprofile=coverage.out
# 生成HTML覆盖率报告
go tool cover -html=coverage.out -o coverage.html
# 竞态检测
go test -race ./...
5. API文档(Swagger/OpenAPI简述)
5.1 OpenAPI规范简介
OpenAPI(原Swagger)是描述RESTful API的标准规范,用YAML/JSON格式定义接口:
# docs/api.yaml(简化版)
openapi: "3.0.3"
info:
title: TaskFlow API
description: 任务管理系统RESTful API
version: "1.0.0"
contact:
name: 开发团队
email: dev@taskflow.example.com
servers:
- url: http://localhost:8080
description: 本地开发环境
paths:
/api/tasks:
get:
summary: 获取任务列表
operationId: listTasks
tags: [任务管理]
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, in_progress, completed]
- name: page
in: query
schema:
type: integer
default: 1
- name: page_size
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: 成功返回任务列表
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Task"
total:
type: integer
page:
type: integer
post:
summary: 创建新任务
operationId: createTask
tags: [任务管理]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTaskRequest"
responses:
"201":
description: 任务创建成功
"400":
description: 请求参数错误
/api/tasks/{id}:
get:
summary: 获取单个任务
operationId: getTask
tags: [任务管理]
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: 成功
"404":
description: 任务不存在
put:
summary: 更新任务
operationId: updateTask
tags: [任务管理]
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: 更新成功
"404":
description: 任务不存在
delete:
summary: 删除任务
operationId: deleteTask
tags: [任务管理]
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: 删除成功
"404":
description: 任务不存在
components:
schemas:
Task:
type: object
properties:
id:
type: integer
example: 1
title:
type: string
example: "学习Go语言"
description:
type: string
example: "完成第30课综合项目"
status:
type: string
enum: [pending, in_progress, completed]
priority:
type: string
enum: [low, medium, high, urgent]
due_date:
type: string
format: date-time
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
CreateTaskRequest:
type: object
required: [title]
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
priority:
type: string
enum: [low, medium, high, urgent]
default: medium
due_date:
type: string
format: date-time
nullable: true
securitySchemes:
BearerAuth:
type: http
scheme: bearer
security:
- BearerAuth: []
5.2 使用Swaggo自动生成文档
对于Go项目,推荐使用 swaggo/swag 从代码注释自动生成Swagger文档:
// 在handler函数上方添加注释
// ListTasks godoc
// @Summary 获取任务列表
// @Description 支持按状态过滤和分页
// @Tags 任务管理
// @Accept json
// @Produce json
// @Param status query string false "任务状态"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} ErrorResponse
// @Security BearerAuth
// @Router /api/tasks [get]
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
// ...
}
生成命令:
# 安装swag工具
go install github.com/swaggo/swag/cmd/swag@latest
# 在项目根目录生成文档
swag init -g cmd/server/main.go
# 生成的文件在 docs/ 目录下
# docs/swagger.json, docs/swagger.yaml, docs/docs.go
6. README撰写规范
一个优秀的README应包含以下部分:
# TaskFlow - 任务管理系统
> 基于Go语言构建的轻量级任务管理RESTful API服务
## 功能特性
- 任务CRUD完整操作
- 按状态、优先级过滤
- 分页查询支持
- API密钥认证
- CORS跨域支持
- 结构化日志记录
- SQLite持久化存储
- Docker一键部署
## 快速开始
### 前置条件
- Go 1.22+
- SQLite3(开发环境)
### 安装与运行
```bash
# 克隆项目
git clone https://github.com/yourname/taskflow.git
cd taskflow
# 安装依赖
go mod tidy
# 运行服务
go run cmd/server/main.go
# 服务默认监听 http://localhost:8080
环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT |
8080 |
服务端口 |
DB_PATH |
taskflow.db |
SQLite数据库文件路径 |
API_KEY |
dev-secret-key |
API认证密钥 |
API调用示例
# 创建任务
curl -X POST http://localhost:8080/api/tasks \
-H "Authorization: Bearer dev-secret-key" \
-H "Content-Type: application/json" \
-d '{"title":"学习Go","priority":"high"}'
# 查询列表
curl http://localhost:8080/api/tasks \
-H "Authorization: Bearer dev-secret-key"
# 健康检查(无需认证)
curl http://localhost:8080/health
项目结构
taskflow/
├── cmd/server/ # 程序入口
├── internal/
│ ├── model/ # 数据模型
│ ├── store/ # 数据存储层
│ ├── service/ # 业务逻辑层
│ ├── handler/ # HTTP处理器层
│ └── middleware/ # 中间件
├── docs/ # API文档
├── Dockerfile
└── README.md
测试
# 运行所有测试
go test ./... -v
# 查看覆盖率
go test ./... -cover
# 竞态检测
go test -race ./...
部署
Docker部署
# 构建镜像
docker build -t taskflow:latest .
# 运行容器
docker run -d \
-p 8080:8080 \
-e API_KEY=your-production-key \
-e DB_PATH=/data/taskflow.db \
-v taskflow-data:/data \
--name taskflow \
taskflow:latest
许可证
MIT License
> 💡 README写作原则:先让别人能在30秒内跑起来,再逐步展开细节。
---
## 7. Dockerfile打包发布
### 7.1 多阶段构建
```dockerfile
# Dockerfile
# ========== 阶段1:构建 ==========
FROM golang:1.22-alpine AS builder
# 安装SQLite编译依赖
RUN apk add --no-cache gcc musl-dev
WORKDIR /app
# 先复制依赖文件,利用Docker缓存层
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并编译
COPY . .
RUN CGO_ENABLED=1 GOOS=linux go build \
-ldflags="-s -w" \
-o /app/taskflow \
./cmd/server/main.go
# ========== 阶段2:运行 ==========
FROM alpine:3.19
# 安装运行时依赖
RUN apk add --no-cache ca-certificates tzdata
# 创建非root用户
RUN adduser -D -g '' appuser
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/taskflow .
# 切换到非root用户
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
# 启动命令
ENTRYPOINT ["./taskflow"]
7.2 Docker Compose配置
# docker-compose.yml
version: "3.8"
services:
taskflow:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- DB_PATH=/data/taskflow.db
- API_KEY=${API_KEY:-change-me-in-production}
- TZ=Asia/Shanghai
volumes:
- taskflow-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
volumes:
taskflow-data:
7.3 构建与运行
# 构建Docker镜像
docker build -t taskflow:latest .
# 查看镜像大小
docker images taskflow
# 使用Docker Compose启动
export API_KEY=your-secret-key
docker compose up -d
# 查看日志
docker compose logs -f
# 停止服务
docker compose down
# 停止并删除数据卷
docker compose down -v
7.4 交叉编译(无Docker环境)
# 编译Linux二进制(在Windows/macOS上)
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
CC=x86_64-linux-musl-gcc \
go build -ldflags="-s -w" -o taskflow-linux ./cmd/server/main.go
# 编译无CGO版本(不支持SQLite,需改用纯Go数据库驱动)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o taskflow-linux ./cmd/server/main.go
8. 部署指南
8.1 部署检查清单
✅ 安全检查
□ 生产环境API_KEY已更换为强密码
□ CORS配置限制了允许的来源域名
□ 日志中不记录敏感信息
□ 使用HTTPS(配置反向代理)
✅ 性能检查
□ 数据库连接池已配置
□ HTTP超时已设置(Read/Write/Idle)
□ 请求体大小已限制
□ 分页查询有最大限制
✅ 可靠性检查
□ 健康检查端点可用
□ 优雅关闭逻辑已实现
□ 日志格式为结构化JSON
□ Docker健康检查已配置
8.2 使用Nginx反向代理
# /etc/nginx/conf.d/taskflow.conf
server {
listen 80;
server_name api.taskflow.example.com;
# 强制HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.taskflow.example.com;
ssl_certificate /etc/ssl/certs/taskflow.crt;
ssl_certificate_key /etc/ssl/private/taskflow.key;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
}
❓ 常见问题
Q1:SQLite在Docker中报错"database is locked"?
A: SQLite对并发写入有限制。确保:
// 1. 设置连接池为1(单连接)
db.SetMaxOpenConns(1)
// 2. 启用WAL模式(允许读写并发)
db.Exec("PRAGMA journal_mode=WAL")
// 3. 设置忙等待超时
// 连接字符串中添加 ?_busy_timeout=5000
如果并发量较大,建议切换到 PostgreSQL。
Q2:中间件的执行顺序有讲究吗?
A: 有。推荐顺序:CORS → 日志 → 认证 → 业务处理。原因:
- CORS 必须最先处理,预检请求(OPTIONS)不携带认证信息
- 日志在认证之前,可以记录未通过认证的请求
- 认证在业务处理之前,拦截非法请求
// Chain 中最先传入的中间件最先执行
handler := middleware.Chain(mux,
middleware.CORS(), // 1. 最先执行
middleware.Logging(), // 2. 其次
middleware.Auth(key), // 3. 最后执行
)
Q3:如何在生产环境中管理API密钥?
A: 永远不要将密钥硬编码或提交到代码仓库。推荐方式:
# 方式1:环境变量
export API_KEY=$(openssl rand -hex 32)
# 方式2:.env文件(加入.gitignore)
echo "API_KEY=$(openssl rand -hex 32)" > .env
# 方式3:Docker Secrets(Swarm模式)
echo "my-secret-key" | docker secret create api_key -
Q4:项目该用SQLite还是PostgreSQL?
A: 根据场景选择:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 学习/原型开发 | SQLite | 零配置,单文件 |
| 内部工具/低流量 | SQLite | 够用且维护简单 |
| 多服务共享数据 | PostgreSQL | 网络访问,集中管理 |
| 高并发写入 | PostgreSQL | SQLite写入串行 |
| 需要JSON查询 | PostgreSQL | JSONB支持强大 |
📖 课程总结
恭喜你完成了全部30节Go语言教程!让我们回顾整个知识体系:
Phase 1 — Go入门(第1-6课)
| 课程 | 核心收获 |
|---|---|
| Go简介 | Go的定位:云原生时代的系统级语言 |
| 变量与类型 | :=短变量、零值机制、iota枚举 |
| 控制流 | for是唯一循环、defer延迟执行、switch无fallthrough |
| 函数 | 多返回值、闭包、init函数、函数是一等公民 |
| 数组与切片 | 切片是引用类型、append扩容机制、底层原理 |
| Map | comma ok模式、无序性、与slice的选型 |
Phase 2 — 结构体与接口(第7-12课)
| 课程 | 核心收获 |
|---|---|
| 结构体 | 值类型vs指针、结构体标签(Tag)、匿名字段 |
| 方法 | 值接收者vs指针接收者、组合优于继承 |
| 接口 | 隐式实现(鸭子类型)、类型断言、接口组合 |
| 错误处理 | error接口、errors.Is/As、自定义错误、显式错误处理哲学 |
| 包与模块 | go mod、导出规则、internal包 |
| 实战 | 结构体+接口+错误处理的综合运用 |
Phase 3 — 并发编程(第13-18课)
| 课程 | 核心收获 |
|---|---|
| Goroutine | 轻量级协程、sync.WaitGroup、泄漏防范 |
| Channel | 无缓冲vs有缓冲、close/range、方向限制 |
| Select | 多路复用、超时控制、fan-in/fan-out模式 |
| sync包 | Mutex/RWMutex、Once、sync.Map、竞态检测 |
| 任务调度器 | context.Context、并发协作、超时重试 |
| 网页爬虫 | 限流、去重、错误重试、优雅退出 |
Phase 4 — 标准库与实用技能(第19-24课)
| 课程 | 核心收获 |
|---|---|
| 字符串处理 | strings/strconv包、strings.Builder |
| 文件IO | os包、bufio高效读写、目录遍历 |
| JSON处理 | Marshal/Unmarshal、结构体标签、流式处理 |
| HTTP编程 | net/http服务器、Handler接口、中间件模式 |
| 测试 | 表驱动测试、httptest、基准测试、覆盖率 |
| 正则与日期 | regexp包、time格式化与解析、定时器 |
Phase 5 — 综合项目(第25-30课)
| 课程 | 核心收获 |
|---|---|
| CLI工具 | flag包、cobra库、参数校验 |
| REST API | RESTful设计、JSON处理、中间件链 |
| 数据库 | database/sql、SQLite驱动、CRUD封装 |
| 部署优化 | 交叉编译、Docker构建、优雅关闭 |
| 综合项目(上) | 架构设计、目录结构、业务逻辑、单元测试 |
| 综合项目(下) | API完善、中间件集成、集成测试、容器化发布 |
核心能力清单
学完30课,你已掌握:
✅ Go语言基础语法与类型系统
✅ 面向对象编程(结构体+接口)
✅ 并发编程(Goroutine+Channel+sync)
✅ 标准库核心包(net/http、encoding/json、testing等)
✅ RESTful API设计与开发
✅ 数据库操作(SQL驱动、CRUD封装)
✅ 测试驱动开发(单元测试、集成测试、基准测试)
✅ 项目工程化(目录结构、依赖管理、配置管理)
✅ 容器化部署(Docker、多阶段构建、健康检查)
📝 后续学习建议
进阶方向
| 方向 | 推荐资源 | 说明 |
|---|---|---|
| Web框架 | Gin、Echo | 生产级HTTP框架 |
| ORM | GORM、sqlx | 数据库操作简化 |
| 微服务 | go-kit、Kratos、go-zero | 微服务框架 |
| 配置管理 | Viper | 多格式配置读取 |
| 日志 | zap、zerolog | 高性能日志库 |
| API网关 | Kong、Traefik | 流量管理 |
| 云原生 | Kubernetes、Docker Compose、Helm | 容器编排 |
| 消息队列 | NATS、Kafka、RabbitMQ | 异步通信 |
推荐书籍
- 《Go程序设计语言》(The Go Programming Language)— Donovan & Kernighan
- 《Go语言高级编程》 — 柴树杉、曹春晖
- 《Concurrency in Go》 — Katherine Cox-Buday
- 《Go语言实战》 — William Kennedy
推荐在线资源
实战项目建议
| 难度 | 项目 | 练习重点 |
|---|---|---|
| ⭐⭐ | URL缩短服务 | HTTP、JSON、SQLite |
| ⭐⭐⭐ | 聊天室 | WebSocket、Goroutine、Channel |
| ⭐⭐⭐ | 个人博客系统 | 模板引擎、Session、文件上传 |
| ⭐⭐⭐⭐ | 分布式爬虫 | 多节点协作、消息队列、去重 |
| ⭐⭐⭐⭐ | API网关 | 中间件链、负载均衡、限流熔断 |
🎉 结语
"少即是多"(Less is more)— Go语言的设计哲学
30节课,从 fmt.Println("Hello, World!") 到构建一个可部署的RESTful API服务,你已经走过了完整的Go语言学习之路。
Go的魅力在于简洁——它不会给你太多选择,但每个选择都经过深思熟虑。当你面对并发问题时,想想goroutine和channel;当你面对错误处理时,想想"显式优于隐式";当你设计接口时,想想"鸭子类型"和"组合优于继承"。
编程是一门手艺,最好的学习方式就是写代码。把这30课的示例都敲一遍,把课后练习都做一遍,再给自己找一个真实的小项目练手——你会发现,Go比你想象的更简单、更强大。
╔══════════════════════════════════════╗
║ 恭喜完成 Go 语言30课完整教程! ║
║ 你已具备开发生产级Go应用的基础。 ║
║ Go build something amazing! ║
╚══════════════════════════════════════╝
下一课
本课是最后一节。如果你想回顾课程起点,请回到 第1课:Go简介。
如有问题或建议,欢迎在 GitHub Issues 提出反馈。



