标题看起来比较混乱,但总的来说是围绕着 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 1 天前
第二点,应该在 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 1 天前
是否写入日志,应该是应用层最上层的开发去调用,这里我认为不需要。
我认为不需要包裹: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 1 天前
能拿到堆栈的前提下,第一点日志没什么必要
|
4
soul11201 1 天前 via Android
return 的两个 error 要不要细分,细分的话,用哨兵比较合适
个人经验,仅供参考 1.日志不要再中间链路打印,有可能会冗余 2.错误如果比较多,用哨兵,如果比较深,用包裹。又深又多,哨兵+错误链包裹 主要目标还是看你目标是什么,比如要把多少异常信息传递给上层、要不要基于底层错误信息在上层做控制处理 |
5
rower 1 天前
第一个对于 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 1 天前
第二点,就是首先需要有自己的自定义错误类型,参考
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 1 天前
正确做法是:
http:业务抛弃 http status code (俗称大码),使用业务小码区分业务错误。 grpc:使用 rpc status extension 。 任何情况下 http 大码都应该作为 i/egress Transport 层的状态表示,业务返回统一以 http 200 完成。 然后,在这个基础上,再去考虑 error 封装问题。 |
9
zzzzaaa 19 小时 45 分钟前
中间件层可以定义具体的错误码告诉调用方,而调用方在错误的时候追加日志,个人理解
|
10
guanzhangzhang 18 小时 43 分钟前
@matrix1010 666 ,感谢分享
|
11
wujianhua22 18 小时 27 分钟前
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 17 小时 35 分钟前 via Android
@aababc #13 需要把错误处理纳入整体考虑了,我当时是响应码约定+错误哨兵+包裹+错误链+中间件(转化、处理、日志) 一套组合拳下来搞定的。
|
19
soul11201 17 小时 11 分钟前
@aababc 把目标定清楚,follow your heart 开干吧💪。这段时间一直在医院,不然可以发你下我当时的代码样例交流下。里面会有一些不太好处理的实现细节,现在可以试试 llm 来辅助生成,当时我都是看看开源库和标准库都怎么做的。
|
20
lvlongxiang199 17 小时 7 分钟前
"是要么写入日志要么返回错误,而不应该两件事情都干。" 可以同时干呀.
就这个例子来说, 可以把返回的 err 记录到 resp.body 里头, 加个中间件记录 access log. 按你这套方式记录日志太麻烦, 而且还不能把 log 跟请求串联起来 |
21
mcfog 16 小时 46 分钟前
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 14 小时 37 分钟前 via iPhone
@wujianhua22 想问下你们这个 recovery 是加在哪里? http 拦截器那里吗? panic 如果没兜住,程序不是直接挂了🤔
|