11 KiB
转岗计划 · 第 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 |
统一响应格式:
{ "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/apisrc/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 生成数据访问代码。因为本周用接口隔离了存储层,下周只需要新写一个实现,接口代码基本不用动。