标题看起来比较混乱,但总的来说是围绕着 error 的设计问题 所有的示例都是围绕着这个 demo 来讨论, 这个 demo 的大意就想创建一个用户,但是在创建用户之前需要检测一下用户的手机号是否存在
func CreateUser(mobile string) (*User, error) {
exists, err := mobileExists(mobile)
if err != nil {
return nil, err
}
if exists {
return nil, fmt.Errorf("User already exists")
}
// ...
}
第一个问题是在 return error 的时候要不要写入日志,代码要不要变成这样
func CreateUser(mobile string) (*User, error) {
exists, err := mobileExists(mobile)
if err != nil {
logger.Errorf("can not close the response", err)
return nil, err
}
if exists {
return nil, fmt.Errorf("User already exists")
}
// ...
}
我得想法总的来说是这样的,要不要把 error 写入日志这个事应该是调用的人来负责,而不是被调用的人来负责,我得想法总的来说 是要么写入日志要么返回错误,而不应该两件事情都干。不知道这个想法对不对?日志的返回这里要不要使用 fmt.Errorf("CreateUser Fail: %w", err)
再返回,扩展开就是什么情况下需要包裹一下
第二个点是关于错误如何和 HTTP 的 Status 关联起来
比如第一个 exists, err := mobileExists(mobile)
这里返回的 err 我希望是一个 HTTP 500 的错误信息,这个点我希望的是非业务层的错误返回 500 比如数据连接失败,redis 连接失败。而且 HTTP 的错误信息还需要返回自己定义的信息。
但是 if exists { return nil, fmt.Errorf("User already exists") }
这个我却希望是一个 HTTP 400 的错误。这个只是举例的这一个 error ,但是内部单纯的业务层面的就有几十个 error 。但是我发现好像不知道怎么做到这一点。
1
Ayanokouji 6 天前
第二点,应该在 handler 层处理,如果你用的是 echo ,
可以 return echo.NewHTTPError( http.StatusUnauthorized, "Please provide valid credentials") https://echo.labstack.com/docs/error-handling 如果是其他框架,比如 gin ,得先判断 error ,然后 c.JSON( http.StatusUnauthorized, "Please provide valid credentials") |
2
mainjzb 6 天前
是否写入日志,应该是应用层最上层的开发去调用,这里我认为不需要。
我认为不需要包裹:fmt.Errorf ,如果需要包裹,也是调用这个函数的人去包裹。这里返回的错误已经清晰明了。 返回 500 还是 400 可以用 errors.as 或 errors.is 去判断,这里的情况应该在 redis 连接部分定义 InternelServerError 返回后用 errors.as 判断。 type InternelServerError struct{ msg string } func (e InternelServerError ) Error() string { return fmt.Sprintf("%v", e.msg) } |
3
Goooooos 6 天前
能拿到堆栈的前提下,第一点日志没什么必要
|
4
soul11201 6 天前 via Android
return 的两个 error 要不要细分,细分的话,用哨兵比较合适
个人经验,仅供参考 1.日志不要再中间链路打印,有可能会冗余 2.错误如果比较多,用哨兵,如果比较深,用包裹。又深又多,哨兵+错误链包裹 主要目标还是看你目标是什么,比如要把多少异常信息传递给上层、要不要基于底层错误信息在上层做控制处理 |
5
rower 6 天前
第一个对于 web 的错误,比较好的做法是创建一个 Error 的中间件统一处理,在 gin 中,我的用法如下
func CreateUser(mobile string) (*User) { exists, err := mobileExists(mobile) if err != nil { // 这个 c 是 gin 的 context ,一般 mobile 这个请求参数是从 c 获得的,这里忽略那些细节,记录错误就是 c.Error() c.Error(err) return nil } if exists { c.Error(err) return nil } // ... } // 中间件处理错误 func Errors(log *logger.Logger) gin.HandlerFunc { return func(c *gin.Context) { ctx := c.Request.Context() if len(c.Errors) > 0 { // 处理第一个错误 // 在 gin 中,错误是一个数组,这里只处理第一个错误,一般来说我们在程序中遇到错误时,只会返回一个错误 // 如果出现了例外情况,那么我们需要修改这里的代码 err := c.Errors[0].Err // 记录错误 log.Error(ctx, "message", "ERROR", err.Error()) } } |
6
rower 6 天前
第二点,就是首先需要有自己的自定义错误类型,参考
https://github.com/ardanlabs/service6-video/tree/main/app/api/errs 然后每种错误对应的 http 状态码 参考 https://github.com/ardanlabs/service6-video/blob/main/api/http/api/mid/errors.go 的 init() 函数 最后在 Error 的中间件中对错误进行判断,如果是自定义错误,返回错误和对应的状态码,如果不是,返回 500 |
7
povsister 6 天前
正确做法是:
http:业务抛弃 http status code (俗称大码),使用业务小码区分业务错误。 grpc:使用 rpc status extension 。 任何情况下 http 大码都应该作为 i/egress Transport 层的状态表示,业务返回统一以 http 200 完成。 然后,在这个基础上,再去考虑 error 封装问题。 |
9
zzzzaaa 6 天前
中间件层可以定义具体的错误码告诉调用方,而调用方在错误的时候追加日志,个人理解
|
10
guanzhangzhang 6 天前
@matrix1010 666 ,感谢分享
|
11
wujianhua22 6 天前
1 、使用 github.com/pkg/errors 处理错误,这样就有堆栈信息了,因此基本不需要打印错误日志,我们错误日志都是在最上层统一处理。
2 、非业务逻辑我们使用 panic 处理,在出口进行 recovery 拦截,这样就可以打印统一 500 错误消息。业务逻辑判断我们定义专门 error 结构体,包含 statusCode ,code 和 message 等信息,还是在出口统一判断,然后进行处理。 tips:我们使用的 kratos 框架 |
13
aababc OP @soul11201 #4 这个用 wrap 现在遇到一个问题,就是他会侵入到我自己的错误信息中,我只想要在日志中体现这个错误,而不想在错误信息中体现底层到底是啥错误
|
14
aababc OP @rower #5 我们现在的分层上来说,是也框架解耦的,createUser 这个可能是 command 调用,也可能是 api 调用,这样的方法是脱离具体的框架的
|
15
aababc OP @povsister #7 这个要不要使用 http code 感觉没有绝对的正确和错误,比如有些人就认为 http 是一个传输协议 http code 代表的 http 协议本身的成功和失败,那我们认为 http 是一个业务协议可以承载我们的业务信息
|
16
aababc OP @matrix1010 #8 感谢,这个我好好看看
|
17
soul11201 6 天前 via Android
@aababc #13 需要把错误处理纳入整体考虑了,我当时是响应码约定+错误哨兵+包裹+错误链+中间件(转化、处理、日志) 一套组合拳下来搞定的。
|
19
soul11201 6 天前
@aababc 把目标定清楚,follow your heart 开干吧💪。这段时间一直在医院,不然可以发你下我当时的代码样例交流下。里面会有一些不太好处理的实现细节,现在可以试试 llm 来辅助生成,当时我都是看看开源库和标准库都怎么做的。
|
20
lvlongxiang199 6 天前
"是要么写入日志要么返回错误,而不应该两件事情都干。" 可以同时干呀.
就这个例子来说, 可以把返回的 err 记录到 resp.body 里头, 加个中间件记录 access log. 按你这套方式记录日志太麻烦, 而且还不能把 log 跟请求串联起来 |
21
mcfog 6 天前
error 是一个 interface ,玩好 error 和掌握 interface 密不可分
比如你想要增强 error 的能力,区分出给用户的信息、HTTP Status ,那么就定一个返回这些信息的 interface ,然后使用这个 interface 来串联:理解错误上下文的模块用实体类型包裹错误,输出错误的外层用接口判断 https://go.dev/play/p/eXY-qna7Ek9 很容易继续扩展 1) 接口是任意组合的 duck typing 因此可以后续任意增加其他能力。比如为 API 场景输出 json 2) errors.As 错误链(官方支持 errors.Join 了,甚至可以是图)可以继续包裹、装饰或者覆盖这些能力。比如翻译中间件可以判断有 UserMessage 就走一下翻译,包一层把报错信息翻译成用户语言 |
23
aababc OP @mcfog #21 也看到了 Join 方法,error interface 本身是比较简单的,现在就是在想这怎么把这些东西组合在一起,如果要丰富 error 的能力就要借助断言或者反射,感觉好像不太喜欢用这些
|
24
Charlie17Li 6 天前 via iPhone
@wujianhua22 想问下你们这个 recovery 是加在哪里? http 拦截器那里吗? panic 如果没兜住,程序不是直接挂了🤔
|
25
povsister 5 天前
@aababc #15
只能说没踩过坑的人才会喜欢 RESTful API 设计。。http 就该老老实实当 transport ,别乱参与业务逻辑了。不然规模上去有的你头秃。 anyway ,你喜欢就好。 |
26
aababc OP @povsister #25 刚看到你回复,我们公司的规模和体量确实比较小,技术的积累也没有那么多,所以设计的方案基本上都是参考其他公司的开放 api ,比如 微信支付 api ( v3 ),支付宝,stripe ,twilio 等对接过的公司的开放的 api ,发现他们有使用 http code 的,有不使用的。
我们对比了一下综合的就选了使用贴近 http code 的方式。使用下来的总的感受是没有遇到啥问题,可能就像你说的规模比较小可能也遇到问题。 我们使用下来的感觉是让上下游的处理更方便的,比如现在遇到 400 首先的猜测就是不是发起调用方的问题,遇到 500 那就首先排查是不是被调用方的问题。然后细节的问题就看具体返回的内容来判定。 感觉运维的监控做起来也比较容易,它只是单纯的关注 http code 就能知道大概的问题。比如遇到大量的 404 的错误,那就要怀疑是不是有人在循环抓取数据,遇到大量的 422 就怀疑是不是接口参数有问题,500 就要怀疑是不是服务的依赖组件有问题或者代码本身的问题。 能否分享一下你们遇到啥问题,才导致放弃来 http code 而使用全 200 加自定义错误的信息 |
27
povsister 5 天前
@aababc
你列举的正是问题所在之一。 API 数量上升之后,服务治理将是一个非常头痛的问题,稍有不慎服务状态就和基础设施状态耦合进去了。你使用 http 状态码会加剧这一过程。 举个例子,http 504 是 gateway timeout ,但业务逻辑执行超时是很常见的现象,现在微服务框架都具备在链路超时 quota 超过后主动取消请求的能力,并且返回 deadline exceed 错误。按照你的想法,那它应该被设定为返回 http 504 。 ok ,记住上面的结论。 现实中,微服务和微服务间存在非常多的 middlebox ( router/Switch/L4/L7 LB ),他们会透明化的按照某些规则转发 http 请求。 假设,有一天中间某个 L7 负载均衡故障,造成 http 转发产生 504 超时。 请问:你怎么判断这个 504 是基础设施故障还是你业务逻辑故障? 以上是可观测问题,下面继续说深刻点。关于 SLO 治理。 正确的方式是让对的人去处理对的事情,而不是服务故障牵一发而动全身。因为你已经混淆了业务响应和基础设施问题,那服务出现故障告警时,运维和开发都会被拉进去。告警噪音将彻底击溃整个系统开发和 SRE 的基本信任。 ok ,到这都还是只讲了 http 。 那如果引入更多的应用层协议,使用 gRPC ,使用 thrift 时,虽然他们都是使用 http transport ,但并不遵守你那套 http status 要求,那你的告警和观测系统要各自做一套吗? 综上,最好的办法是,业务独立使用一套自己定义的错误观测体系,所有的应用层协议都按 transport 层处理。明确基础设施和业务边界 |
28
aababc OP @povsister #27 假设现在有两个角色,nginx -> server 当 nginx 不能拿到 server 的结果的时候应该是 504 吧,但是如果 nginx 能拿到 server 的结果但是 server 自己内部处理超时比如数据查询超时,这时候是不是返回 500 更合适
|
29
wujianhua22 4 天前
@Charlie17Li Kratos 、gin 这种 recovery 都是在中间件。其他的大概率也是。
|
30
Kauruus 3 小时 49 分钟前
Nginx 返回 504 Gateway Timeout 是因为 Nginx 自己是 Gateway/Proxy ,返回 504 让 Nginx 的客户端知道 nginx timeout 了,没有从上游获拿到响应。
除非你业务是做代理,做请求转发,不然业务逻辑故障不应该返回 504 。 |