个人主页会编程的土豆欢迎来访作者简介后端学习者❄️个人专栏数据结构与算法数据库leetcode✨那些你一个人走过的夜路终将化作照亮未来的光对应代码main.go路由、handlers/接口函数、dao/数据库、utils/json.go统一返回适合对象刚开始写后端接口、分不清「该先写哪一层」的同学写在前面很多人一上来就问「写接口到底先写路由还是先写数据库」其实有两件事要分开说法指的是什么请求跑起来时的顺序路由 → Handler → 取参数 → DAO → 返回响应动手写代码时的顺序可以跟请求从上往下写也可以先建表再往上接本篇只讲第一件事——一次请求进来之后代码是怎么一层层往下走的。这也是最容易建立直觉的顺序先挂门铃路由再写接待员Handler再让他去仓库拿货DAO最后把货递给客人返回读完你应该能自己说清楚「一个接口完整走过哪 5 步」对照项目里GET /api/movie指着代码讲一遍照着这个模板自己加一个简单接口一、先用生活例子建立印象把影院票务服务器想成一家电影院前台步骤生活里代码里1. 路由门口牌子「查电影详情请走 3 号窗口」r.GET(/api/movie, ...)2. Handler3 号窗口的工作人员APIMovie这个函数3. 取参数工作人员问你「要查哪部电影的 id」c.Query(id)4. DAO工作人员去仓库/电脑查库存dao.GetMovie(id)5. 返回把结果告诉你或说「没有这部」utils.OK/utils.Fail客人只跟前台说话不会自己冲进仓库翻货同理浏览器/Postman 只打 HTTP不会直接调 daodao 只被 Handler 调用整条链路画成图客户端浏览器 / Postman │ │ GET /api/movie?id1 ▼ ┌───────────────────┐ │ main.go 路由表 │ ← 第 1 步匹配路径和方法 └─────────┬─────────┘ │ 找到对应函数 ▼ ┌───────────────────┐ │ handlers.APIMovie │ ← 第 2 步处理这次请求 └─────────┬─────────┘ │ 取出 id1 ▼ ┌───────────────────┐ │ dao.GetMovie │ ← 第 4 步查数据库 └─────────┬─────────┘ │ 查出电影 或 报错 ▼ ┌───────────────────┐ │ utils.OK / Fail │ ← 第 5 步写成 JSON 返回 └─────────┬─────────┘ │ ▼ 客户端收到 JSON中间「取参数」就是第 3 步嵌在 Handler 里面二、第 1 步写路由告诉服务器「这个地址找谁」2.1 路由是什么路由 「请求方法和路径」→「哪个函数来处理」的对照表。没有路由Handler 写得再好外面也永远调不到——就像前台员工在但门口没挂「3 号窗口」的牌子。本项目路由集中在main.go例如r.GET(/api/movie, app.APIMovie) // 查一部电影 r.GET(/api/movies, app.APIMovies) // 电影列表 r.POST(/api/order/create, app.APICreateOrder) // 下单读法很直白r.GET只接受 GET 请求/api/movie路径必须对上app.APIMovie对上了就执行这个函数2.2 GET 和 POST 怎么选方法常见用途项目例子GET查询、读数据参数常挂在 URL 上/api/movie?id1POST提交、写数据参数常放在 JSON body 里/api/login、/api/order/create注意Postman 里如果路由是POST你却用GET去打会404——不是业务错了是方法和路径没对上路由表。2.3 写接口时为什么很多人先写这一行因为先挂路由你立刻可以用 Postman 打一下看是不是 404Handler 先写空壳返回还没做完确认「路通了」再往下补取参、DAO、真正业务这叫从上往下写先打通管道再往管道里填逻辑。三、第 2 步写 Handler真正接待这次请求3.1 Handler 是什么Handler 处理「这一次 HTTP 请求」的函数。在 Gin 里签名通常是func (a *App) APIMovie(c *gin.Context) { // ... }c *gin.Context可以理解为「这一次请求的工具箱」能读 URL 参数、JSON body能读 Cookie / Session能往回写 JSON、HTML本项目里API 类 Handler 一般放在handlers/下名字常以API开头方便和页面函数PageXxx区分名字返回什么给谁用APIMovieJSON前端 JS / PostmanPageMovieDetailHTML 页面浏览器直接打开3.2 Handler 里通常干什么固定套路绝大多数接口Handler 里就干这几件事按顺序① 鉴权要不要登录要不要管理员 ② 取参数Query / JSON / 路径参数 ③ 业务校验参数合不合法、状态对不对 ④ 调用 dao真正读写数据库 ⑤ 返回 OK 或 Fail不是每个接口都要鉴权公开查询可以没有 ①。但② → ④ → ⑤几乎总有。3.3 对照真实代码查电影详情handlers/movie.go里的APIMoviefunc (a *App) APIMovie(c *gin.Context) { id, _ : strconv.ParseInt(c.Query(id), 10, 64) // 第 3 步取参数 m, err : dao.GetMovie(id) // 第 4 步调 DAO if err ! nil { utils.Fail(c, err.Error()) // 第 5 步失败返回 return } if m nil { utils.Fail(c, 电影不存在) return } utils.OK(c, m) // 第 5 步成功返回 }再加上main.go里的r.GET(/api/movie, app.APIMovie) // 第 1 步路由五步就齐了这个接口不需要登录所以没有鉴权逻辑很短最适合初学时整段背下来。、四、第 3 步获取数据从请求里拿出「原料」「获取数据」不是去数据库拿而是从这次 HTTP 请求里把客户端传来的参数读出来。数据库那一步叫 DAO这一步叫取参 / 绑参。4.1 常见三种取参方式① URL 查询参数Query—— GET 最常用请求GET /api/movie?id1idStr : c.Query(id) // 得到字符串 1 id, _ : strconv.ParseInt(idStr, 10, 64) // 转成 int64项目里列表接口也这样// GET /api/movies?q流浪group2 keyword : c.Query(q) groupID, _ : strconv.ParseInt(c.Query(group), 10, 64)② JSON Body—— POST 最常用请求体{ schedule_id: 10, seat_ids: [1, 2, 3] }Handler 里var req struct { ScheduleID int64 json:schedule_id SeatIDs []int64 json:seat_ids } if err : c.ShouldBindJSON(req); err ! nil { utils.Fail(c, 参数错误) return }json:schedule_id的意思是JSON 里那个字段名对应到结构体这个字段。绑失败格式不对、不是 JSON就直接 Fail别往下跑。③ 路径参数Path—— 页面路由常见例如页面GET /movie/:id路径里自带 id。API 本项目多用 Query路径参数更多用在PageMovieDetail这类页面上。4.2 取完参数往往还要「校验」取参成功 ≠ 业务可用。常见校验校验例子必填片名不能为空范围时长必须 0存在性场次是否存在权限是不是本人的订单状态场次是否已取消、是否已开场例如下单APICreateOrder取完schedule_id、seat_ids之后还会查场次、判断是否已开场——这些都属于「业务校验」通常仍写在 Handler 里真正改座位、插订单才进 DAO。4.3 和「鉴权」的关系鉴权也是「从请求里拿信息」只是拿的是登录态Session / Cookie不是业务参数u : a.currentUser(c) if u nil { utils.Fail(c, 请先登录) return }管理员接口可以写成if a.apiUser(c, true) nil { return // 里面已经 Fail 过了 }可以记取业务参数用户想干什么查哪部电影、订哪些座鉴权这个人是谁、有没有资格干五、第 4 步写 DAO真正和数据库打交道5.1 DAO 是什么DAOData Access Object 专门负责数据库读写的一层。本项目放在dao/包。Handler 不写 SQL/GORM 细节只调用m, err : dao.GetMovie(id)dao.GetMovie内部再用 GORM 查表func GetMovie(id int64) (*models.Movie, error) { var m models.Movie err : movieQuery().Where(m.id ?, id).Scan(m).Error if err ! nil { return nil, err } if m.ID 0 { return nil, nil // 没找到约定返回 (nil, nil) } return m, nil }5.2 为什么要单独一层不直接在 Handler 里查库好处说明职责清晰Handler 管 HTTPDAO 管数据库好复用页面PageMovieDetail和接口APIMovie都能调同一个GetMovie好改以后换查询写法主要改 dao不用每个 Handler 翻一遍好测复杂逻辑如下单事务集中在 dao方便单独想清楚新手可以先记硬规矩Handler 里出现db.DB/ 大段 GORM多半该挪到 dao。5.3 DAO 函数一般长什么样常见模式func Xxx(...) (结果类型, error)调用方固定写法结果, err : dao.Xxx(...) if err ! nil { utils.Fail(c, err.Error()) return } // 再根据「结果是否为空」做业务判断本项目里约定举例GetMovie找不到时(nil, nil)由 Handler 说「电影不存在」有的函数直接error把业务失败原因放在err.Error()里如下单锁座失败六、第 5 步返回响应把结果变成 JSON 交给客户端6.1 本项目的统一 JSON 格式utils/json.gotype APIResult struct { Code int json:code Message string json:message Data interface{} json:data,omitempty } func OK(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, APIResult{Code: 0, Message: ok, Data: data}) } func Fail(c *gin.Context, message string) { c.JSON(http.StatusOK, APIResult{Code: 1, Message: message, Data: nil}) }客户端看什么字段含义code 0成功code 1业务失败未登录、参数错、电影不存在……message给人看的说明data成功时的数据失败时常为 null成功示例{ code: 0, message: ok, data: { id: 1, title: 某部电影, duration: 120 } }失败示例{ code: 1, message: 电影不存在, data: null }6.2 为什么失败也经常 HTTP 200本项目里OK/Fail都用http.StatusOK200用code字段区分成败。前端/Postman 习惯先看 HTTP 通不通再看code是不是 0。有的公司会用 401、404 等 HTTP 状态码表达业务两种风格都有跟项目约定走即可。6.3 Handler 里写返回时的好习惯失败立刻return避免继续执行后面逻辑成功只在一条路径OK结构更清晰错误信息写人话「请先登录」「电影不存在」不要丢一长串英文给用户七、用「查电影」把五步串成一条故事假设 Postman 请求GET http://127.0.0.1:8080/api/movie?id1路由Gin 看到 GET /api/movie找到app.APIMovie进 Handler开始执行APIMovie取参c.Query(id)→1→id 1DAOdao.GetMovie(1)去 MySQL 查movies并关联分组名等返回查到了 →utils.OK(c, m)→code:0 电影 JSON没查到 →utils.Fail(c, 电影不存在)→code:1数据库挂了 →utils.Fail(c, err.Error())你对着这条链路讲一遍就等于把「接口完整流程」讲明白了。八、再用「下单」看一个稍复杂的例子下单比查电影多了「必须登录」和「更多校验」但骨架不变。8.1 路由r.POST(/api/order/create, app.APICreateOrder)8.2 Handler 骨架按五步标注func (a *App) APICreateOrder(c *gin.Context) { // ② 旁路鉴权也是从请求里拿登录态 u : a.currentUser(c) if u nil { utils.Fail(c, 请先登录) return } // ③ 取参数JSON body var req struct { ScheduleID int64 json:schedule_id SeatIDs []int64 json:seat_ids } if err : c.ShouldBindJSON(req); err ! nil { utils.Fail(c, 参数错误) return } // ③ 继续业务校验场次是否存在、是否已开场…… sch, err : dao.GetSchedule(req.ScheduleID) // ... 各种 if Fail ... // ④ 真正写库事务锁座 建订单 order, err : dao.CreateOrderWithSeats(...) if err ! nil { utils.Fail(c, err.Error()) return } // ⑤ 返回 utils.OK(c, order) }复杂接口 同样五步只是第 3、4 步更长。别被长度吓到拆开看还是「路由 → Handler → 取参校验 → DAO → 返回」。九、动手写代码时建议怎么落笔既然运行顺序是从上到下写的时候也可以从上到下每一步都能立刻验证推荐练习顺序以「根据 id 查分组」为例① 先写路由r.GET(/api/group, app.APIGroup)② 写空 Handler先能返回func (a *App) APIGroup(c *gin.Context) { utils.OK(c, gin.H{tip: 接口通了逻辑还没写}) }Postman 打一下看到 JSON → 说明路由对了。③ 取参数id, _ : strconv.ParseInt(c.Query(id), 10, 64) if id 0 { utils.Fail(c, id 无效) return }④ 补 DAOg, err : dao.GetGroup(id) // 若还没有就新建这个函数⑤ 完整返回if err ! nil { utils.Fail(c, err.Error()) return } if g nil { utils.Fail(c, 分组不存在) return } utils.OK(c, g)每加一层就测一层比「五层写完再测」更容易定位错在哪。十、和「先建表再写」不冲突有同学会问学长不是说要先建表吗视角顺序请求怎么跑本篇路由 → Handler → 取参 → DAO → 返回数据从哪来建表视角先有表 / ModelDAO 才有东西可查可以这么记对外从上往下想客户端怎么打进来对内库表是地基没有表DAO 查空气实际项目里常常是表大体有了 → 你按本篇五步加接口缺字段再回头改表