V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
lix7
V2EX  ›  Go 编程语言

请教大家几个 Go 写业务的工程实践的上的问题

  •  
  •   lix7 · 2022-07-01 16:30:21 +08:00 · 4159 次点击
    这是一个创建于 658 天前的主题,其中的信息可能已经有所发展或是发生改变。

    自己是 Java 后台开发,尝试用 Go 写了个小项目,有几个工程实践上的问题想咨询下各位都是怎么处理的?

    1. 如何统一各组件的 log 输出?例如我用 echo 、xorm 、grpc 去构建业务,这三个库的日志格式各自不同,Go 生态中有没有类似 slf4j 的东西能让三者用同一种日志规范输出?
    2. 如何在 log 中附上当前每一个 http 请求的 requestId 呢?不能是靠 Context 层层往下传吧? Java 中是依赖于日志框架 MDC 实现的,相关 requestId 会绑定在线程上,线程模型可能会有性能问题,但对于开发者写代码来说还是挺友好的。
    3. 如何妥善的处理异常?业务代码中的常见操作就是读数据、执行业务逻辑、保存新数据,这个过程中任何一步有异常我都可以认为是业务处理异常,Java 中只需要把这三步全部 try catch 起来就行了(只是个例子,实际处理粒度不会这么大),但 Go 中只能每一步都冒泡,然后写很多次 if err != nil 么?

    抛开工程上的这些问题,Go 还是蛮好的,找回了刚上学时候写 C 的快感,真就是单纯地在一行一行写代码,感觉每一行都能映射到汇编上,掌控性很强。感觉更适合强规范的基础设施开发和工具开发。

    接下来想去看看 Rust ,不知道会不会遇到类似的问题。毕竟语言再好,最终还是要用来干活儿的。对于业务开发,CRUD boy 来说,写代码和查问题这种场景搞不定,语言再好也是麻烦。

    第 1 条附言  ·  2023-03-13 13:53:25 +08:00
    又写了半年的 Go ,感觉这三样本质上都不是什么问题,其实也恰恰是 Go 语言和 Java 思想上最不一样的地方,不评价谁好谁坏,只是两者思路有别。

    - Java 中,我们习惯构建一个全局上下文 MDC ,当前请求的信息都可以绑定到 MDC 上;而 Go 选择的是使用 ctx 显式向下传递。
    - Java 中,遇到异常可以直接通过异常的方式从调用栈的任何位置抛出; Go 选择是让开发者手动的将错误冒泡至需要的位置,向上传递。

    context 和 err 是 Go 显式控制数据流向下、向下传递的两大方式,只能说和 Java 有差异,但非要在 Go 里寻求 Java 的体验也属于是给自己找不痛快了。不过说实话,对于业务研发,我感觉还是 Java 的方式更方便一些,带来的缺陷就是太依赖于线程上下文了,一旦这个假设不成立,开发体验指数级下降。

    对于日志组件来说,Java 中各个组件通过反射方法检测 slf4j ,然后去判断使用哪一种日志实现。这在 Go 里就没法做了,自然只能手动指定,从依赖注入的角度来说,也没有任何问题。

    Java 现如今学习成本高,我个人认为和各种反射、线程上下文的滥用脱离不开关系,从日志实现、JPA 到 Spring Context 等等都一样。如果遇到同时还喜欢在 MDC 里乱塞东西的话,那体验更差(当然 Go 的 context 里塞东西也差不多)。

    希望能给有相关困惑的同学一些思路,共勉。
    20 条回复    2022-07-02 23:37:34 +08:00
    lix7
        1
    lix7  
    OP
       2022-07-01 16:32:23 +08:00
    看到这么一篇文章,貌似日志规范这个事情在 2022 年还没有什么解决方案

    [聊聊 Go 应用输出日志的工程实践 | Tony Bai]( https://tonybai.com/2022/03/05/go-logging-practice/)
    MoYi123
        2
    MoYi123  
       2022-07-01 16:37:58 +08:00
    1. 比如 xorm 有个 setlogger 的功能, 需要把你的 logger 按他的 interface 封装一下, 就可以接入统一的日志了, grpc 和 echo 同理. 一般都会有这个功能, 但是不排除某些第三方库不支持.

    2. 一般都是 context 传

    3. 多数情况就是 if err != nil,或者用 recover 也行吧.
    lix7
        3
    lix7  
    OP
       2022-07-01 16:49:05 +08:00
    @MoYi123 感谢,那看来和我想到的差不太多。社区都认可这些实践嘛,总感觉差了点意思...
    BeautifulSoap
        4
    BeautifulSoap  
       2022-07-01 16:50:07 +08:00
    1. 没有。不同包往往都有自己的日志输出逻辑,完全看包的作者

    2. requestId 就是 context 层层往下传。context 的作用之一就是这个这么做没关系的,算是 Go 的标准做法。并且你要知道,Go 协程、Go 协程,这里不是 Go 多线程。不同的网络请求很可能会跑在同一个线程里的

    3. err 一层层往上返回也是 Go 的标准做法,if err != nil 有人觉得麻烦有人觉得无所谓(我属于后者)。不过因为现在官方 go 的 errors 包不含调用栈信息,所以实际上用得最多的还是 github.com/pkg/errors 这个包
    IIInsomnia
        5
    IIInsomnia  
       2022-07-01 17:04:14 +08:00
    可以参考一下这个: https://github.com/shenghui0779/tplgo
    yc8332
        6
    yc8332  
       2022-07-01 17:22:06 +08:00
    你说的 java 的是生态决定的吧,只是那些包都支持了那个日志组件。
    LoNeFong
        7
    LoNeFong  
       2022-07-01 17:38:54 +08:00
    1.基本正经的库都不会乱打日志,一般都是 error 级别的才有,捕获 wrap 往上抛出即可,业务代码中使用的是 zap ,提出全局的 logger 配置到公共库
    2.我研究过好多 web 框架 requestId 都是通过 ctx 传递的,这种做法也还是合理的,毕竟一层层传递 ctx 不光是为了一个 requestId
    3.v 站有很严重的争议问题,怎么说怎么有理,我选择优先处理错误: https://go.dev/blog/errors-are-values
    haolongsun
        8
    haolongsun  
       2022-07-01 17:49:54 +08:00
    rust 做的非常好,标准库只定义了 log trait ,然后让第三方库去自己实现,这样不管什么框架用的啥,都要回归到标准 log ,这样统一全局。
    janxin
        9
    janxin  
       2022-07-01 18:08:09 +08:00
    第一个问题是生态软件没有一统江湖的选择,然而事实上也不能这么依赖第三方的依赖,毕竟这个选择权归作者所有,作者不用就抓瞎。Go 的第三方库基本这个问题就是看是否有预定抽象接口,替换一下即可。不过这也是作者行为,作者不允许你改,你能做的只有重定向输出。

    第二个问题建议使用 context 下传,后面很多东西比较方便处理。如果实在不想,也有 gls 这些第三方方案可以用。但是这些方案不保证兼容性。周边生态基本是 context 兼容的,如果使用 gls 很难和这些方案结合,比如分布式追踪。选择 gls 另外一个问题和 goroutine 有关,新 goroutine 的创建时就需要额外处理很多东西了。这个在并发时会经常遇到。

    第三个问题无解,就是辣么烦。不过想偷懒的话,Go1.18 之后有第三方库快速处理一下,比如 https://pkg.go.dev/github.com/samber/lo#readme-try
    lix7
        10
    lix7  
    OP
       2022-07-01 18:11:51 +08:00
    @IIInsomnia 看了下,感觉还不错,很规整,star 了
    lix7
        11
    lix7  
    OP
       2022-07-01 18:14:32 +08:00
    @yc8332 但是基本上 Java 生态里所有的三方组件都会支持这个日志组件,已经是生态里日志规范的事实标准了。Go 发展也有快十年了,出现这样一个社区标准也不意外
    lix7
        12
    lix7  
    OP
       2022-07-01 18:17:49 +08:00
    @MoYi123
    @LoNeFong
    @janxin
    感谢三位,还是得再观望下社区接下来的发展了
    lix7
        13
    lix7  
    OP
       2022-07-01 18:18:14 +08:00
    @haolongsun 这个也太 nice 了!简直是理想状态了,我去了解了解
    KevinBlandy
        14
    KevinBlandy  
       2022-07-01 18:37:09 +08:00
    我想补充一个问题:
    Go 里面如何优雅的处理事务呢?最好能实现类似于 spring 那种事务管理,自动回滚,提交。并且事务方法之间的调用保证在同一个事务之中。
    haolongsun
        15
    haolongsun  
       2022-07-01 19:47:06 +08:00
    haolongsun
        16
    haolongsun  
       2022-07-01 19:48:21 +08:00
    guoer
        17
    guoer  
       2022-07-01 19:58:29 +08:00
    无解

    其实 1,2,3 中的痛点都是 Go 的推荐做法 XD
    blless
        18
    blless  
       2022-07-02 15:00:26 +08:00
    @KevinBlandy 我们以前是 web 框架用 gin ,事务封装成一个 gin 中间件,进入业务处理前先获取一个 db 事务对象,业务处理完成后提交就行。如果是无状态的业务同时有多个进程,中间可能要加个 redis 分布式锁,事务提交前要先检查锁是否过期,过期了就不能提交。
    blless
        19
    blless  
       2022-07-02 15:10:40 +08:00
    1 、各个组件的日志看组件有没有提供 Logger 接口,有的话一般是把全局的 Logger 单独实现一个组件的 Logger 然后传进去,但是其实我们以前公司是不允许组件输出太多 Log 的。不然很容易就导致日志量暴表。
    2 、据我所知大部分框架 RequestId 之类的还真是靠 context 往下传的,context 其实在 go 里面真的很有用,因为协程的生命周期都需要用 context 来控制。基本上你可以认为 context 就是用来跟协程进行绑定的东西,你不用 context 往下传,协程处理的生命周期就会断开,导致一些未知的 bug.
    3 、Java 也不是什么地方都可以随便 Try catch 的,正常业务异常都需要 throw 出去,不然可能会丢失原始的错误信息,导致出现 bug 的时候无法排查。少数比如网络重试之类的异常可以直接 catch 掉 重试,go 里面 你要想省事就直接 进入协程处理业务进去的时候写一个 recover ,然后业务里面出错直接 panic 。我们以前就这么干,web 业务应用无所谓的。但是基础组件,中间件,我们不允许 panic
    lix7
        20
    lix7  
    OP
       2022-07-02 23:37:34 +08:00
    @guoer 苦一苦了,不过这几个点也可能是我目前思维问题,适应下 Go 的方式应该问题也不大
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3932 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 10:16 · PVG 18:16 · LAX 03:16 · JFK 06:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.