golang 中 error 如何影响 log 和 api 状态

39 天前
 aababc

标题看起来比较混乱,但总的来说是围绕着 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 。但是我发现好像不知道怎么做到这一点。

1723 次点击
所在节点    程序员
30 条回复
Ayanokouji
39 天前
第二点,应该在 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")
mainjzb
39 天前
是否写入日志,应该是应用层最上层的开发去调用,这里我认为不需要。
我认为不需要包裹:fmt.Errorf ,如果需要包裹,也是调用这个函数的人去包裹。这里返回的错误已经清晰明了。

返回 500 还是 400 可以用 errors.aserrors.is 去判断,这里的情况应该在 redis 连接部分定义 InternelServerError 返回后用 errors.as 判断。

type InternelServerError struct{
msg string
}
func (e InternelServerError ) Error() string {
return fmt.Sprintf("%v", e.msg)
}
Goooooos
39 天前
能拿到堆栈的前提下,第一点日志没什么必要
soul11201
39 天前
return 的两个 error 要不要细分,细分的话,用哨兵比较合适

个人经验,仅供参考
1.日志不要再中间链路打印,有可能会冗余
2.错误如果比较多,用哨兵,如果比较深,用包裹。又深又多,哨兵+错误链包裹

主要目标还是看你目标是什么,比如要把多少异常信息传递给上层、要不要基于底层错误信息在上层做控制处理
rower
39 天前
第一个对于 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())
}


}
rower
39 天前
第二点,就是首先需要有自己的自定义错误类型,参考
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
povsister
39 天前
正确做法是:
http:业务抛弃 http status code (俗称大码),使用业务小码区分业务错误。
grpc:使用 rpc status extension 。

任何情况下 http 大码都应该作为 i/egress Transport 层的状态表示,业务返回统一以 http 200 完成。
然后,在这个基础上,再去考虑 error 封装问题。
matrix1010
39 天前
zzzzaaa
39 天前
中间件层可以定义具体的错误码告诉调用方,而调用方在错误的时候追加日志,个人理解
guanzhangzhang
39 天前
@matrix1010 666 ,感谢分享
wujianhua22
39 天前
1 、使用 github.com/pkg/errors 处理错误,这样就有堆栈信息了,因此基本不需要打印错误日志,我们错误日志都是在最上层统一处理。
2 、非业务逻辑我们使用 panic 处理,在出口进行 recovery 拦截,这样就可以打印统一 500 错误消息。业务逻辑判断我们定义专门 error 结构体,包含 statusCode ,code 和 message 等信息,还是在出口统一判断,然后进行处理。

tips:我们使用的 kratos 框架
aababc
39 天前
@mainjzb #2 这个意思就是需要自己自定义错误,封装自己需要的信息,感觉 go error 这一块一直不太容易把握
aababc
39 天前
@soul11201 #4 这个用 wrap 现在遇到一个问题,就是他会侵入到我自己的错误信息中,我只想要在日志中体现这个错误,而不想在错误信息中体现底层到底是啥错误
aababc
39 天前
@rower #5 我们现在的分层上来说,是也框架解耦的,createUser 这个可能是 command 调用,也可能是 api 调用,这样的方法是脱离具体的框架的
aababc
39 天前
@povsister #7 这个要不要使用 http code 感觉没有绝对的正确和错误,比如有些人就认为 http 是一个传输协议 http code 代表的 http 协议本身的成功和失败,那我们认为 http 是一个业务协议可以承载我们的业务信息
aababc
39 天前
@matrix1010 #8 感谢,这个我好好看看
soul11201
39 天前
@aababc #13 需要把错误处理纳入整体考虑了,我当时是响应码约定+错误哨兵+包裹+错误链+中间件(转化、处理、日志) 一套组合拳下来搞定的。
aababc
39 天前
@soul11201 #17 我们现在就在干这个事儿,所以就想看看大家的意见,以及有没有比较好的实践
soul11201
39 天前
@aababc 把目标定清楚,follow your heart 开干吧💪。这段时间一直在医院,不然可以发你下我当时的代码样例交流下。里面会有一些不太好处理的实现细节,现在可以试试 llm 来辅助生成,当时我都是看看开源库和标准库都怎么做的。
lvlongxiang199
39 天前
"是要么写入日志要么返回错误,而不应该两件事情都干。" 可以同时干呀.
就这个例子来说, 可以把返回的 err 记录到 resp.body 里头, 加个中间件记录 access log. 按你这套方式记录日志太麻烦, 而且还不能把 log 跟请求串联起来

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1096495

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX