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

286 天前
 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 。但是我发现好像不知道怎么做到这一点。

2406 次点击
所在节点    程序员
30 条回复
mcfog
285 天前
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 就走一下翻译,包一层把报错信息翻译成用户语言
aababc
285 天前
@soul11201 #19 感谢,等有时间了可以分享一下看看,我写 go 的路子比较野,现在就想看看比较好的规划是啥样的
aababc
285 天前
@mcfog #21 也看到了 Join 方法,error interface 本身是比较简单的,现在就是在想这怎么把这些东西组合在一起,如果要丰富 error 的能力就要借助断言或者反射,感觉好像不太喜欢用这些
Charlie17Li
285 天前
@wujianhua22 想问下你们这个 recovery 是加在哪里? http 拦截器那里吗? panic 如果没兜住,程序不是直接挂了🤔
povsister
285 天前
@aababc #15
只能说没踩过坑的人才会喜欢 RESTful API 设计。。http 就该老老实实当 transport ,别乱参与业务逻辑了。不然规模上去有的你头秃。
anyway ,你喜欢就好。
aababc
285 天前
@povsister #25 刚看到你回复,我们公司的规模和体量确实比较小,技术的积累也没有那么多,所以设计的方案基本上都是参考其他公司的开放 api ,比如 微信支付 api ( v3 ),支付宝,stripe ,twilio 等对接过的公司的开放的 api ,发现他们有使用 http code 的,有不使用的。
我们对比了一下综合的就选了使用贴近 http code 的方式。使用下来的总的感受是没有遇到啥问题,可能就像你说的规模比较小可能也遇到问题。
我们使用下来的感觉是让上下游的处理更方便的,比如现在遇到 400 首先的猜测就是不是发起调用方的问题,遇到 500 那就首先排查是不是被调用方的问题。然后细节的问题就看具体返回的内容来判定。
感觉运维的监控做起来也比较容易,它只是单纯的关注 http code 就能知道大概的问题。比如遇到大量的 404 的错误,那就要怀疑是不是有人在循环抓取数据,遇到大量的 422 就怀疑是不是接口参数有问题,500 就要怀疑是不是服务的依赖组件有问题或者代码本身的问题。
能否分享一下你们遇到啥问题,才导致放弃来 http code 而使用全 200 加自定义错误的信息
povsister
284 天前
@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 层处理。明确基础设施和业务边界
aababc
284 天前
@povsister #27 假设现在有两个角色,nginx -> server 当 nginx 不能拿到 server 的结果的时候应该是 504 吧,但是如果 nginx 能拿到 server 的结果但是 server 自己内部处理超时比如数据查询超时,这时候是不是返回 500 更合适
wujianhua22
284 天前
@Charlie17Li Kratos 、gin 这种 recovery 都是在中间件。其他的大概率也是。
Kauruus
280 天前
Nginx 返回 504 Gateway Timeout 是因为 Nginx 自己是 Gateway/Proxy ,返回 504 让 Nginx 的客户端知道 nginx timeout 了,没有从上游获拿到响应。

除非你业务是做代理,做请求转发,不然业务逻辑故障不应该返回 504 。

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

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

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

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

© 2021 V2EX