当前位置: 首页 > news >正文

后端接口错误码到底该怎么设计?我见过最烂的和最优雅的两种方案

后端接口错误码到底该怎么设计?我见过最烂的和最优雅的两种方案

标签:#接口设计#后端开发#最佳实践#代码规范
适合:所有写 RESTful / RPC 接口的后端开发者


一个真实故事

新人小王刚入职,接到一个需求:写一个"用户充值"接口。

写完接口,前端同学拿来调,调通后开始抓狂

"为什么余额不足返回 -1,参数错误返回 400,账号封禁返回 -99999?" "errno 是数字还是字符串?我看你这接口给了我个 "0001"" "为什么有时候返回 {code: 0},有时候返回 {errno: 0}?" "成功时 data 是数组,失败时 data 是 null,我前端要写多少 if?"

小王挠头。这就是缺少错误码规范的痛苦。


我见过最烂的 3 种错误码设计

烂方案 1:用 HTTP 状态码表达业务错误

HTTP/1.1 400 Bad Request {"error": "余额不足"}
HTTP/1.1 403 Forbidden {"error": "兑换码已使用"}

问题

  • HTTP 400/403/500 都是协议层错误,不该表达业务
  • 网关、CDN、监控工具都会对 4xx/5xx 报警 → 业务正常报错变运维告警
  • 接口聚合时(一个网关聚合多个接口)状态码冲突
  • HTTP 状态码就那么几个,业务错误几百个,根本不够分

烂方案 2:错误码"野生生长"

// 接口 A:{"err":-1,"msg":"..."}// 接口 B:{"code":"FAIL","data":null}// 接口 C:{"status":false,"info":{...}}// 接口 D(成功):{"errno":0,"data":[...]}

问题

  • 前端每个接口都要单独写错误判断
  • 拦截器/统一处理写不出来
  • 团队协作灾难

烂方案 3:错误码 + 错误消息散落各处

// handler1.goreturnResponse{Code:-1,Msg:"参数错误"}// handler2.goreturnResponse{Code:10001,Msg:"参数错"}// 同样的语义,不同的码和消息// handler3.goreturnResponse{Code:-1,Msg:"兑换码不存在"}// 复用了 -1,但语义不同

问题

  • 错误码无法穷举
  • 同一个错误在不同接口码不同
  • 文档永远跟不上代码

最优雅的方案:集中注册的错误码体系

设计原则

  1. 错误码就是数字— 不要用字符串
  2. 业务错误一律 HTTP 200— HTTP 状态码只表达协议层错误
  3. 响应格式统一{errno, errmsg, data}三件套
  4. 错误码集中注册— 一个文件管所有错误码 + 对应文案
  5. 错误码按业务模块分段— 一眼看出错误来自哪个模块

实现示例

1. 集中注册错误码(errors.go
packageerrorsvarErrorMessages=map[int]string{// ── 通用错误(10000-19999)──10001:"内部错误",10002:"参数错误",10003:"解析失败",10010:"验证失败",11001:"未登录",11002:"权限不足",// ── DB / 缓存错误(12000-13999)──12001:"数据库连接失败",12003:"数据库操作失败",13001:"Redis 操作失败",// ── 兑换业务(40000-40999)──40001:"兑换失败",40002:"兑换码无效",40003:"兑换码已被锁定",40005:"已购买过该商品",40007:"当前平台不支持兑换此商品",// ── 会员业务(50000-50999)──50001:"不是会员",50002:"会员已过期",50003:"权益已用完",// ── 限流(60000-60999)──60001:"请求过于频繁",}// Show 根据错误码渲染响应funcShow(codeint)[]byte{msg,ok:=ErrorMessages[code]if!ok{msg="未知错误"}res,_:=json.Marshal(map[string]interface{}{"errno":code,"errmsg":msg,"data":nil,})returnres}// ShowSuc 成功响应funcShowSuc(datainterface{})[]byte{res,_:=json.Marshal(map[string]interface{}{"errno":0,"errmsg":"ok","data":data,})returnres}
2. 统一响应封装(response.go
typeBaseResponseWriterstruct{}func(b*BaseResponseWriter)RenderJsonErrno(w http.ResponseWriter,codeint){w.Header().Set("Content-Type","application/json; charset=UTF-8")w.Write(errors.Show(code))}func(b*BaseResponseWriter)RenderJsonSuc(w http.ResponseWriter,datainterface{}){w.Header().Set("Content-Type","application/json; charset=UTF-8")w.Write(errors.ShowSuc(data))}
3. Handler 调用(清爽到没朋友)
func(h*ExchangeHandler)Exchange(w http.ResponseWriter,r*http.Request){cdkey:=r.URL.Query().Get("cdkey")ifcdkey==""{h.RenderJsonErrno(w,10002)// 参数错误return}errno,data:=exchangeService.Exchange(userId,cdkey)iferrno!=0{h.RenderJsonErrno(w,errno)// 业务错误,直接返回错误码return}h.RenderJsonSuc(w,data)}
4. Service 返回 (errno, data) 而不是 error
func(s*ExchangeService)Exchange(userId,cdkeystring)(int,*Result){record,err:=keyRepo.GetByCdkey(cdkey)iferr!=nil{return12003,nil}// DB 错误ifrecord==nil{return40002,nil}// 兑换码无效ifrecord.Status!=Available{return40002,nil}if!keyRepo.Lock(userId,record.Id){return40003,nil// 锁定失败}orderNo,payCode:=payClient.CreateOrder(...)ifpayCode==ErrAlreadyOwned{return40005,nil// 已购买}return0,&Result{OrderNo:orderNo}}

为什么 Service 返回 (errno, data) 而不是 error?

这点很多人不理解,我详细讲讲:

方案 A:返回 error

func(s*Service)Exchange(...)(*Result,error){if...{returnnil,ErrCdkeyInvalid}if...{returnnil,ErrLocked}}// Handler 要做映射errno:=ErrToErrno(err)// ❌ 又要写一遍映射函数h.RenderJsonErrno(w,errno)

方案 B:返回 (errno, data)

func(s*Service)Exchange(...)(int,*Result){if...{return40002,nil}if...{return40003,nil}}// Handler 直接转发h.RenderJsonErrno(w,errno)

优势

  • 错误码即文档,Service 一看就知道返什么码
  • Handler 不需要做 error → errno 映射
  • 业务错误码本来就是给客户端的,提前确定编号更自然

何时仍然用 error

  • 工具函数(如aes.Decrypt
  • Model 层(数据库错误)
  • External 层(外部调用错误)

业务接口用 errno,底层工具用 error,分工明确


错误码分段约定(必看)

按业务模块划分错误码段,一眼看出问题在哪

用途
0成功
10000-19999通用错误(参数、登录、权限)
12000-13999数据存储相关(DB、Redis)
14000-19999系统错误(限流、熔断)
20000-29999用户中心
30000-39999支付
40000-49999兑换、虚拟商品
50000-59999会员、权益
60000+各业务自由扩展

排查问题时,看到errno: 40003,立刻知道是兑换业务的"锁定失败"。


客户端如何统一处理?

错误码体系最大的好处是客户端可以写统一拦截器

// 前端拦截器(axios 示例)axios.interceptors.response.use(resp=>{if(resp.data.errno!==0){// 统一的错误处理if(resp.data.errno===11001){redirectToLogin()}elseif(resp.data.errno>=60000&&resp.data.errno<70000){showToast("请求过于频繁,请稍后")}else{showToast(resp.data.errmsg)}returnPromise.reject(resp.data)}returnresp.data.data})

业务代码完全不用关心错误处理:

constdata=awaitapi.exchange({cdkey:'XXX'})// 直接拿到 data,错误已被拦截器处理

一些"血泪经验"

经验 1:永远不要复用错误码

// ❌ 危险constErrFail=-1returnErrFail// 兑换失败也用这个,登录失败也用这个// ✅ 正确constErrExchangeFail=40001constErrLoginFail=11001

排查时按错误码搜代码,一秒定位。

经验 2:错误码文档要在线生成

// 在错误码定义旁边加注释,工具自动生成 markdown 文档const(// 10002 参数错误:请求参数缺失或格式不对ErrInvalidParam=10002// 40002 兑换码无效:兑换码不存在、已使用或已过期ErrCdkeyInvalid=40002)

经验 3:日志一定带错误码

log.Errorf("exchange failed, errno=%d, userId=%s, cdkey=%s",errno,userId,cdkey)

ELK 里按 errno 聚合,问题分布一目了然

经验 4:监控告警关键错误码

# 监控规则-errno == 12003 且 QPS>10 → DB 异常告警-errno == 60001 且 QPS>100 → 限流告警-errno == 40005 占比>30% → 业务异常告警

小结

错误码设计 5 条铁律:

  1. 业务错误用 HTTP 200,HTTP 状态码只表达协议层
  2. 错误码是数字,集中注册在一个文件
  3. 响应格式三件套{errno, errmsg, data}
  4. 按业务模块分段:10000/20000/30000/…
  5. Service 返回 (errno, data),省掉 error → errno 映射

错误码体系做好了,团队协作效率 × 2,排查问题速度 × 5。


下一篇:Go 微服务里的"客户端代理"是什么?为什么要在启动时初始化?

如果觉得有用,点赞收藏关注 ⭐

http://www.jsqmd.com/news/864530/

相关文章:

  • Betaflight飞控固件2026完全指南:从入门到精通的7个实用技巧
  • NotebookLM智能摘要失真问题(底层token切分逻辑与人工校准SOP)
  • 终极指南:如何免费获取Cursor Pro功能,轻松突破试用限制
  • 浙江话AI语音项目最后通牒:2024Q3起ElevenLabs将关闭非ISO方言模型上传通道,现在必须掌握这5个迁移预案
  • 分布式ID生成方案详解与实战
  • Go 微服务必备:服务发现、配置中心、中间件是怎么协作的?
  • ElevenLabs接入云南话语音合成:从零部署到商用上线的7大关键配置(含昆明/大理/红河三地方言音素映射表)
  • 潮州话TTS落地最后一公里:ElevenLabs音频后处理秘技(含潮汕童谣节奏建模与语义停顿注入)
  • Python Selenium 瀏覽器自動化測試工具
  • 职场新人不会写自我介绍怎么办?AI三分钟帮你搞定,面试邀约直接翻倍!
  • 分享一个专门用于 SAP 开发的 Claude Code Skill 插件集合
  • 端侧AI基础设施:核心环节与代表企业
  • 裸辞转行AI大模型:我的探索与收获,收藏这份经验助你启程!
  • 大模型赋能政务审批:从 “人工审” 到 “智能核”
  • 如果你还在为CAD、SolidWorks的许可发愁,看看这八家
  • 406_C++_磁盘检查流程安全重构分析:从 system/popen 到 fork/exec 的防命令注入升级
  • 观察不同模型在 Taotoken 平台上的响应速度与效果差异
  • 独立开发者如何借助taotoken以更低成本启动ai项目
  • 时序例外:false_path / multicycle_path / max_delay
  • 新手程序员必备:收藏这份GPT大模型学习指南,从入门到精通!
  • 2026企业网盘选型指南:外部协作可控、合规审计、版本追溯的8款测评盘点
  • 昇腾CANN实战:FlashAttention 在昇腾NPU上的实现与性能调优
  • Spek音频频谱分析器:完整指南与实用技巧
  • GitLab CI|CD 配置笔记
  • 游戏化编程教学系统CodeCombat本地化部署实战:构建高效稳定的离线学习环境
  • 2026网盘怎么选:别只盯“不限速”,更该看同步稳定性与数据安全
  • 我用可视化工作流搭了一个发票识别助手,顺便聊聊 AI Agent 落地的那些弯路
  • 2026年AI编程助手综合实力排行榜
  • MySQL 索引数据结构与算法
  • 终极免费桌面分区工具NoFences:告别Windows桌面混乱的完整解决方案