后端接口分层架构:Handler/Service/Model 各自该写什么?写错了会怎样?
后端接口分层架构Handler/Service/Model 各自该写什么写错了会怎样标签#架构设计#代码规范#后端开发#最佳实践适合刚开始写工业级后端代码的同学你是不是经常这样写代码funcExchange(w http.ResponseWriter,r*http.Request){cdkey:r.URL.Query().Get(cdkey)// 解析 cookiecookie,_:r.Cookie(session)userId:decryptSession(cookie.Value)// 查 DBvarrecord CdkeyRecord db.Where(cdkey ?,cdkey).First(record)ifrecord.Status!1{json.NewEncoder(w).Encode(map[string]interface{}{err:invalid})return}// 调支付resp,_:http.Get(http://pay-service/order/create?...)varpayResp PayResp json.NewDecoder(resp.Body).Decode(payResp)// 更新状态db.Model(record).Update(status,3)// ... 又写了 200 行json.NewEncoder(w).Encode(map[string]interface{}{data:...})}能跑但是一个函数 300 行改起来眼花DB 操作和 HTTP 响应混在一起没法写单元测试同样的取用户 ID代码在 50 个接口里复制粘贴新人接手第一句话“这代码我看不懂”这就是缺少分层架构的代价。标准答案四层架构┌─────────────────────────┐ │ ① Handler 接口层 │ ← 只跟 HTTP/RPC 打交道 ├─────────────────────────┤ │ ② Service 业务层 │ ← 只跟业务规则打交道 ├─────────────────────────┤ │ ③ Model 数据层 │ ← 只跟数据库打交道 └─────────────────────────┘ ↓ ┌─────────────────────────┐ │ ④ External 外部依赖层 │ ← 只跟其他服务打交道 └─────────────────────────┘铁律每一层只跟相邻层说话单向依赖不能跨层。每一层的职责清单① Handler 层只做协议适配func(h*ExchangeHandler)Exchange(w http.ResponseWriter,r*http.Request){// 1. 解 Cookieargs,err:cookie.Parse(r)iferr!nil{h.RenderJsonErrno(w,ErrInvalidParam);return}// 2. 鉴权sessionId → userIduserId,err:userClient.GetUserId(args[session_id])iferr!nil{h.RenderJsonErrno(w,ErrUnauthorized);return}// 3. 取业务参数cdkey:r.URL.Query().Get(cdkey)// 4. 调 Serviceerrno,data:exchangeService.Exchange(userId,cdkey)// 5. 渲染响应iferrno!0{h.RenderJsonErrno(w,errno);return}h.RenderJsonSuc(w,data)}只做 5 件事解协议Cookie、Header、Body鉴权取参数调 Service包响应不应该做❌ 直接查 DB❌ 写业务规则“满 100 减 10”❌ 直接调下游服务除了鉴权类❌ 计算“总价 ∑商品价格”② Service 层业务核心func(s*ExchangeService)Exchange(userId,cdkeystring)(int,*Result){// 1. 查数据record,err:keyRepo.GetByCdkey(cdkey)iferr!nil{returnErrCodeInvalid,nil}// 2. 业务规则ifrecord.Status!StatusAvailable{returnErrCodeInvalid,nil}ifrecord.EndTimetime.Now().Unix(){returnErrCodeExpired,nil}// 3. 并发控制状态机if!keyRepo.Lock(userId,record.Id){returnErrLockFailed,nil}// 4. 调下游orderNo,err:payClient.CreateOrder(userId,record.ItemId)iferr!nil{keyRepo.Unlock(userId,record.Id)returnErrOrderFailed,nil}// 5. 写库errassetRepo.Add(userId,record.ItemId)iferr!nil{returnErrAssetFailed,nil}// 6. 返回return0,Result{OrderNo:orderNo,...}}核心职责业务规则、事务编排、并发控制。不应该做❌ 解析 HTTP 参数应该传进来❌ 写 JSON 响应应该返回结构体❌ 直接拼 SQL应该调 Model③ Model 层数据访问funcGetByCdkey(cdkeystring)(*Record,error){db:getDB()varrecord Record err:db.Table(exchange_codes).Where(cdkey ?,cdkey).First(record).Erroriferr!nil{iferrors.Is(err,gorm.ErrRecordNotFound){returnnil,nil}returnnil,err}returnrecord,nil}funcLock(userIdstring,iduint)bool{result:db.Table(exchange_codes).Where(id ? AND user_id ,id).Updates(map[string]interface{}{status:StatusLocked,user_id:userId,})returnresult.RowsAffected0}核心职责纯 DB CRUD不写任何业务逻辑。不应该做❌ 业务判断“如果是 VIP 就…”❌ 调外部服务❌ 复杂数据组装应该交给 Service④ External 层外部调用封装varpayClient HttpClientfuncInitPayClient(){payClientNewClientProxy(pay-service.api)}funcCreateOrder(ctx context.Context,userId,itemIdstring)(string,error){ctx,cancel:context.WithTimeout(ctx,2*time.Second)defercancel()varrsp Response err:payClient.Post(ctx,/order/create,req,rsp)iferr!nil{return,err}returnrsp.OrderNo,nil}核心职责封装外部依赖的调用细节。3 个必备要素服务发现用服务名而非 IP超时控制必设错误监控上报写错了会怎样真实案例反模式 1在 Handler 里写业务// ❌ 错误示范funcExchange(w,r){cdkey:r.URL.Query().Get(cdkey)varrecord Record db.First(record,cdkey ?,cdkey)ifrecord.Status!1||record.EndTimetime.Now().Unix(){...}// 100 行业务逻辑}后果单元测试要 mock HTTP根本写不动同样逻辑在 RPC 接口、定时任务里还要再写一遍改个错误码要改 N 个文件反模式 2Service 里直接 r.URL.Query()// ❌ 错误示范func(s*Service)Exchange(r*http.Request){cdkey:r.URL.Query().Get(cdkey)// ...}后果Service 跟 HTTP 协议耦合没法在 RPC/MQ 里复用写单元测试要构造http.Request痛苦反模式 3Model 里塞业务// ❌ 错误示范func(m*Model)GetCdkey(cdkeystring)(*Record,error){record,err:db.First(...)// ❌ 不应该在这里判断业务规则ifrecord.Status!1{returnnil,errors.New(已过期)}returnrecord,nil}后果Model 没法被多个 Service 复用每个业务对已过期定义不同业务规则散落难维护反模式 4External 不设超时// ❌ 错误示范funcCreateOrder(...)(string,error){resp,err:http.Get(url)// 没有超时// ...}后果下游服务卡住 → 你的服务线程池打满 → 雪崩这是一线大厂故障复盘里最经典的一类事故实战怎么判断代码分层是否正确3 个灵魂拷问1. “把 HTTP 改成 gRPCService 要改吗”✅ 不用改 → 分层正确❌ 要改 → Service 跟协议耦合了2. “把 MySQL 换成 PostgreSQLService 要改吗”✅ 不用改只改 Model→ 分层正确❌ 要改 → Service 跟数据库耦合了3. “Service 的方法能单元测试吗”✅ 能mock 几个 Repo 就行 → 分层正确❌ 不能依赖一堆 HTTP/DB → 没分清楚层次推荐的目录结构project/ ├── cmd/main.go ← 入口 ├── handler/ ← Handler 层 │ ├── v1/ │ │ ├── exchange.go │ │ └── user.go │ └── router.go ├── service/ ← Service 层 │ ├── exchange/ │ │ └── exchange.go │ └── user/ ├── model/ (or repo/) ← Model 层 │ ├── cdkey/ │ │ └── cdkey.go │ └── asset/ ├── external/ (or client/) ← External 层 │ ├── pay/ │ └── user/ ├── common/ ← 错误码、常量 └── pkg/ ← 工具类小结层写什么不写什么Handler解协议、鉴权、参数、响应业务、SQLService业务规则、编排、事务HTTP/SQL 细节Model纯 CRUD业务判断External调外部服务业务逻辑3 条记忆口诀Handler 是搬运工— 把请求搬给 Service把结果搬给客户端Service 是大脑— 所有业务规则在这里Model 是仓库管理员— 只管存取不管价值判断写代码的时候经常自问“这段代码放在这一层合不合适”慢慢就有感觉了。下一篇Go 微服务必备服务发现、配置中心、中间件是怎么协作的如果觉得有用点赞收藏关注 ⭐