V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
aababc
V2EX  ›  程序员

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

  •  
  •   aababc · 1 天前 · 1130 次点击

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

    25 条回复    2024-12-11 19:37:35 +08:00
    Ayanokouji
        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")
    mainjzb
        2
    mainjzb  
       1 天前
    是否写入日志,应该是应用层最上层的开发去调用,这里我认为不需要。
    我认为不需要包裹: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
        3
    Goooooos  
       1 天前
    能拿到堆栈的前提下,第一点日志没什么必要
    soul11201
        4
    soul11201  
       1 天前 via Android
    return 的两个 error 要不要细分,细分的话,用哨兵比较合适

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

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


    }
    rower
        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
    povsister
        7
    povsister  
       1 天前
    正确做法是:
    http:业务抛弃 http status code (俗称大码),使用业务小码区分业务错误。
    grpc:使用 rpc status extension 。

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

    tips:我们使用的 kratos 框架
    aababc
        12
    aababc  
    OP
       18 小时 10 分钟前
    @mainjzb #2 这个意思就是需要自己自定义错误,封装自己需要的信息,感觉 go error 这一块一直不太容易把握
    aababc
        13
    aababc  
    OP
       18 小时 9 分钟前
    @soul11201 #4 这个用 wrap 现在遇到一个问题,就是他会侵入到我自己的错误信息中,我只想要在日志中体现这个错误,而不想在错误信息中体现底层到底是啥错误
    aababc
        14
    aababc  
    OP
       17 小时 57 分钟前
    @rower #5 我们现在的分层上来说,是也框架解耦的,createUser 这个可能是 command 调用,也可能是 api 调用,这样的方法是脱离具体的框架的
    aababc
        15
    aababc  
    OP
       17 小时 49 分钟前
    @povsister #7 这个要不要使用 http code 感觉没有绝对的正确和错误,比如有些人就认为 http 是一个传输协议 http code 代表的 http 协议本身的成功和失败,那我们认为 http 是一个业务协议可以承载我们的业务信息
    aababc
        16
    aababc  
    OP
       17 小时 46 分钟前
    @matrix1010 #8 感谢,这个我好好看看
    soul11201
        17
    soul11201  
       17 小时 35 分钟前 via Android
    @aababc #13 需要把错误处理纳入整体考虑了,我当时是响应码约定+错误哨兵+包裹+错误链+中间件(转化、处理、日志) 一套组合拳下来搞定的。
    aababc
        18
    aababc  
    OP
       17 小时 20 分钟前
    @soul11201 #17 我们现在就在干这个事儿,所以就想看看大家的意见,以及有没有比较好的实践
    soul11201
        19
    soul11201  
       17 小时 11 分钟前
    @aababc 把目标定清楚,follow your heart 开干吧💪。这段时间一直在医院,不然可以发你下我当时的代码样例交流下。里面会有一些不太好处理的实现细节,现在可以试试 llm 来辅助生成,当时我都是看看开源库和标准库都怎么做的。
    lvlongxiang199
        20
    lvlongxiang199  
       17 小时 7 分钟前
    "是要么写入日志要么返回错误,而不应该两件事情都干。" 可以同时干呀.
    就这个例子来说, 可以把返回的 err 记录到 resp.body 里头, 加个中间件记录 access log. 按你这套方式记录日志太麻烦, 而且还不能把 log 跟请求串联起来
    mcfog
        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 就走一下翻译,包一层把报错信息翻译成用户语言
    aababc
        22
    aababc  
    OP
       15 小时 10 分钟前
    @soul11201 #19 感谢,等有时间了可以分享一下看看,我写 go 的路子比较野,现在就想看看比较好的规划是啥样的
    aababc
        23
    aababc  
    OP
       15 小时 8 分钟前
    @mcfog #21 也看到了 Join 方法,error interface 本身是比较简单的,现在就是在想这怎么把这些东西组合在一起,如果要丰富 error 的能力就要借助断言或者反射,感觉好像不太喜欢用这些
    Charlie17Li
        24
    Charlie17Li  
       14 小时 37 分钟前 via iPhone
    @wujianhua22 想问下你们这个 recovery 是加在哪里? http 拦截器那里吗? panic 如果没兜住,程序不是直接挂了🤔
    povsister
        25
    povsister  
       9 小时 0 分钟前
    @aababc #15
    只能说没踩过坑的人才会喜欢 RESTful API 设计。。http 就该老老实实当 transport ,别乱参与业务逻辑了。不然规模上去有的你头秃。
    anyway ,你喜欢就好。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   859 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 20:38 · PVG 04:38 · LAX 12:38 · JFK 15:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.