# 转岗计划 · 第 2 周学习计划(Gin 写 HTTP 接口 + 接前端) > 所属阶段:第一段 Go 全栈基础(第 1~5 周)。总纲见同目录 `README.md`,下周计划见 `week03-postgresql.md`。 目标:把第 1 周的命令行任务清单改造成一个 Web 应用。后端用 Gin 提供 REST 接口,前端用你熟悉的 React 或 Vue 调用接口。本周数据先存在内存里,第 3 周再换成 PostgreSQL。 前置条件:第 1 周的 `todo` 项目已完成,`go test ./...` 能通过。 参考资料: - Gin 官方文档(中文):https://gin-gonic.com/zh-cn/docs/ - Go 标准库 `net/http`:https://pkg.go.dev/net/http --- ## 最终项目结构 ``` 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 | 统一响应格式: ```json { "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 生成数据访问代码。因为本周用接口隔离了存储层,下周只需要新写一个实现,接口代码基本不用动。