Files
zhuangang/plan/week02-gin-api.md
T

11 KiB
Raw Blame History

转岗计划 · 第 2 周学习计划(Gin 写 HTTP 接口 + 接前端)

所属阶段:第一段 Go 全栈基础(第 1~5 周)。总纲见同目录 README.md,下周计划见 week03-postgresql.md。

目标:把第 1 周的命令行任务清单改造成一个 Web 应用。后端用 Gin 提供 REST 接口,前端用你熟悉的 React 或 Vue 调用接口。本周数据先存在内存里,第 3 周再换成 PostgreSQL。

前置条件:第 1 周的 todo 项目已完成,go test ./... 能通过。

参考资料:


最终项目结构

todo-api/
├── backend/
│   ├── go.mod
│   ├── main.go                      # 程序入口:读配置、组装依赖、启动服务
│   ├── internal/
│   │   ├── model/
│   │   │   └── task.go              # Task 结构体和校验
│   │   ├── store/
│   │   │   ├── store.go             # TaskStore 接口定义
│   │   │   └── memory_store.go      # 内存实现(并发安全)
│   │   ├── handler/
│   │   │   ├── task_handler.go      # 任务相关接口
│   │   │   ├── task_handler_test.go # 接口测试
│   │   │   └── response.go          # 统一响应格式
│   │   ├── middleware/
│   │   │   ├── logger.go            # 请求日志
│   │   │   ├── cors.go              # 跨域
│   │   │   └── recovery.go          # 捕获 panic
│   │   └── router/
│   │       └── router.go            # 路由注册
│   └── api.http                     # 手动测试接口用的请求文件
└── frontend/
    ├── package.json
    ├── .env.development             # 接口地址配置
    └── src/
        ├── api/
        │   └── tasks.ts             # 封装所有任务接口请求
        ├── types/
        │   └── task.ts              # Task 类型,和后端保持一致
        ├── components/
        │   ├── TaskForm.tsx         # 新增任务表单
        │   ├── TaskList.tsx         # 任务列表
        │   └── TaskItem.tsx         # 单条任务(完成、删除、编辑)
        └── App.tsx

如果前端用 Vue,把 .tsx 换成 .vue,结构不变。

接口设计(本周要实现的全部接口)

方法 路径 说明 成功状态码
GET /api/health 健康检查 200
GET /api/tasks 获取任务列表,支持 ?done=true/false 过滤 200
GET /api/tasks/:id 获取单个任务 200
POST /api/tasks 新增任务,请求体 {"title": "..."} 201
PATCH /api/tasks/:id 修改标题或完成状态 200
DELETE /api/tasks/:id 删除任务 204

统一响应格式:

{ "code": 0, "message": "ok", "data": { } }

出错时 code 为非 0,message 写清楚原因,例如 {"code": 40401, "message": "task not found", "data": null}。


第 1 天:用标准库理解 HTTP,再上手 Gin

学习内容

  • 先用标准库 net/http 写一个最简单的服务器,理解 http.Handler、ResponseWriter、Request。
  • 再用 Gin 改写,对比两者差别,理解 Gin 帮你省了什么。
  • 安装 Gin:go get github.com/gin-gonic/gin。

要建的文件

  • backend/go.mod:运行 go mod init todo-api。
  • backend/main.go:
    • 先写标准库版本:/hello 返回 Hello, HTTP,跑通后注释掉。
    • 再写 Gin 版本:GET /api/health 返回 {"status": "ok"},监听 8080 端口。
  • backend/api.http:写好 GET http://localhost:8080/api/health 请求,用编辑器的 REST Client 插件或 curl 测试。

运行方式:cd backend && go run .,另开终端运行 curl localhost:8080/api/health

完成标准:能说清楚一个 HTTP 请求从进入 Gin 到返回响应经过了哪些步骤。


第 2 天:数据模型和存储层

学习内容

  • 用接口隔离存储层,让接口代码不关心数据存在哪儿(下周换数据库时只换实现)。
  • 用 sync.RWMutex 让内存存储在并发请求下安全。

要建的文件

  • internal/model/task.go:
    • Task 结构体:ID int64、Title string、Done bool、CreatedAt time.Time、UpdatedAt time.Time,带 JSON 标签。
    • func (t Task) Validate() error:标题不能为空,长度不超过 100 字。
  • internal/store/store.go:
    • var ErrNotFound = errors.New("task not found")
    • 定义 TaskStore 接口:List(done *bool) ([]model.Task, error)、Get(id int64)、Create(t model.Task)、Update(t model.Task)、Delete(id int64)。
  • internal/store/memory_store.go:
    • MemoryStore 结构体:内部用 map[int64]model.Task 存数据,用自增 ID,用 sync.RWMutex 加锁。
    • 实现 TaskStore 接口的全部方法,找不到时返回 ErrNotFound。

完成标准:写一个小测试,启动 100 个 goroutine 同时调用 Create,最后任务数正好是 100,用 go test -race ./... 检查没有数据竞争。


第 3 天:任务接口(增删改查)

学习内容

  • Gin 的路由分组、路径参数 c.Param、查询参数 c.Query。
  • 请求体绑定 c.ShouldBindJSON 和参数校验标签 binding:"required"。
  • 正确使用 HTTP 状态码。

要建的文件

  • internal/handler/response.go:
    • func OK(c *gin.Context, data any)、func Created(c *gin.Context, data any)、func Fail(c *gin.Context, httpStatus int, code int, msg string),统一响应格式。
  • internal/handler/task_handler.go:
    • TaskHandler 结构体,字段是 store.TaskStore 接口。
    • 方法 List、Get、Create、Update、Delete,对应上面的接口表。
    • 请求结构体 CreateTaskRequest、UpdateTaskRequest(Update 里的字段用指针,区分“没传”和“传了空值”)。
    • store.ErrNotFound 转成 404,校验失败转成 400。
  • internal/router/router.go:
    • func New(h *handler.TaskHandler) *gin.Engine,注册 /api 路由组下的全部接口。
  • 更新 main.go:创建 MemoryStore、TaskHandler、路由,启动服务。
  • 更新 api.http:为 6 个接口都写好测试请求,包括错误情况(空标题、不存在的 ID)。

完成标准:用 api.http 或 curl 把每个接口的正常和出错情况都测一遍,状态码和响应格式都正确。


第 4 天:中间件

学习内容

  • Gin 中间件原理:c.Next() 前后分别执行什么。
  • 跨域(CORS)为什么会出现,前端开发时怎么处理。

要建的文件

  • internal/middleware/logger.go:记录每个请求的方法、路径、状态码、耗时,用标准库 log/slog 输出。
  • internal/middleware/cors.go:允许前端开发地址(如 http://localhost:5173)跨域访问,处理 OPTIONS 预检请求。
  • internal/middleware/recovery.go:捕获 panic,返回统一的 500 错误响应,并记录日志。
  • 更新 router.go:用 gin.New() 替代 gin.Default(),手动挂上这三个中间件。

完成标准:故意在一个接口里写 panic,服务不会挂掉,返回 500 和统一格式;日志里能看到每个请求的耗时。


第 5 天:接口测试

学习内容

  • 用 net/http/httptest 在不启动服务的情况下测试接口。
  • 表格驱动测试覆盖多种请求。

要建的文件

  • internal/handler/task_handler_test.go:
    • 写一个 setupRouter() 辅助函数,用 MemoryStore 组装路由。
    • 测试用例至少包括:创建成功返回 201、空标题返回 400、获取不存在的任务返回 404、修改完成状态、删除后再获取返回 404、按 done 过滤列表。

运行方式:go test ./... -v -race

完成标准:全部测试通过,没有数据竞争警告。


第 6 天:前端接入

学习内容

  • 前后端分离的联调方式:开发时用环境变量配置接口地址,或用 Vite 代理。
  • 前端类型和后端 JSON 保持一致。

要建的文件

  • 用 Vite 创建前端:npm create vite@latest frontend -- --template react-ts(Vue 用 vue-ts)。
  • frontend/.env.development:VITE_API_BASE=http://localhost:8080/api
  • src/types/task.ts:Task 类型,字段名和后端 JSON 标签一致。
  • src/api/tasks.ts:封装 listTasks、createTask、updateTask、deleteTask,统一处理响应格式,code 非 0 时抛出错误。
  • src/components/TaskForm.tsx:输入标题,回车或点按钮新增。
  • src/components/TaskItem.tsx:显示单条任务,可勾选完成、删除、双击编辑标题。
  • src/components/TaskList.tsx:显示列表,带“全部 / 未完成 / 已完成”过滤。
  • src/App.tsx:组合以上组件,处理加载状态和错误提示。

运行方式:后端 go run .,前端 npm run dev,浏览器打开前端地址。

完成标准:在页面上能完成增删改查和过滤,后端日志能看到对应请求。


第 7 天:配置、优雅关机和收尾

学习内容

  • 用环境变量配置端口等参数,不把配置写死在代码里。
  • 优雅关机:收到 Ctrl+C 时,等正在处理的请求完成再退出。

要建的文件和修改

  • 更新 main.go:
    • 从环境变量 PORT 读端口,没设置时默认 8080。
    • 用 http.Server 包装 Gin,监听 os.Interrupt 和 syscall.SIGTERM 信号,调用 server.Shutdown 优雅关机,超时 5 秒。
  • todo-api/README.md:写清楚项目功能、技术栈、接口列表、本地运行步骤。
  • 提交并推送到 zhuangang 仓库(路径 todo-api/)。

完成标准

  • go vet ./... 没有警告,gofmt -l . 没有输出。
  • go test ./... -race 全部通过。
  • 前后端联调正常,README 别人照着能跑起来。
  • 在总纲 README.md 的进度记录里勾掉第 2 周。

本周自查清单

  • 能说清楚 Gin 处理一个请求的流程
  • 理解为什么存储层要用接口隔离
  • 会用 RWMutex 写并发安全的结构
  • 会写 REST 接口,状态码使用正确
  • 会写自定义中间件
  • 会用 httptest 写接口测试
  • 前后端联调完成
  • 会做配置读取和优雅关机

下周预告:第 3 周把 MemoryStore 换成 PostgreSQL,学表结构设计、SQL、索引和事务,用 sqlc 生成数据访问代码。因为本周用接口隔离了存储层,下周只需要新写一个实现,接口代码基本不用动。