1
pluswu1986 3 天前
业务代码一般不打印日志, 中间件统一在入口处理,底层错误返回 error.Warp 足够的信息(callstack 等) 让中间件统一处理
|
2
Ayanokouji OP @pluswu1986 用的是 github.com/pkg/errors 这个库吗,但是这个已经不更新了
|
3
csys 3 天前
没懂你这啥问题
go 记录 error 日志,怎么就没有调用栈了 如果 r 缺少调用栈,那是日志库有问题或者用法有问题 |
4
harleyliao 3 天前
一般是在每个出错的地方都打印错误日志, 并返回错误给上一层, 这样通过错误日志就可以推测出调用链了.
|
5
Ayanokouji OP |
6
bli22ard 3 天前
可能是最佳实战的做法是,你调用了标准库,或者第三方库,这些库返回 error 之后,你应该先用一些第三方 error 库 wrap 一下,主要目的是记录一下 error 发生的调用栈,这样上层什么位置拿到 error ,都能打印出来这个 error 是哪个位置发生的。还有一种,目的类似的做法,不记录调用栈,而改为附加一个错误码进去,这样上层的任何调用者也可以知道 error 哪里发生的,不过维护错误码这种方式维护时间越长,越容易搞混乱,导致排查问题困难。
原则上,只要不是本工程的代码生成的 error 都进行一次 wrap ,然后向上 return ,如果没有最上层,则进行错误日志打印。 |
7
Ayanokouji OP |
8
dylanqqt 3 天前
@Ayanokouji 我们就是遇到 err 就打印,“查五次”就要打印五次的 err ,要写五次 if err != nil ,因为可能第一个 err 会影响第二次的查询,不判断没法往下走啊
|
9
Ayanokouji OP @dylanqqt 不能往下走是没问题的,就是五次 if err != nil 里边还有写五次打印日志,遇到错误也只能是其中一处,实际打印的日志也是一次
|
10
csys 3 天前 1
@Ayanokouji
懂你意思了 > 如果遇到 error 就打印 多数情况下是这样的,我自己是倾向于把所有 error 都打印出来,除非某个地方什么都不做,只是把 error 向上传递,也就是说 1. 产生 error 的地方记录日志 2. 处理 error 的地方记录日志 你遇到的这个问题,看起来更像是需要一个链路追踪 https://opentelemetry.io/docs/languages/go/instrumentation/ 至于直接在 error 里记录完整的调用栈也是可以的 https://pkg.go.dev/github.com/cockroachdb/errors |
11
dylanqqt 3 天前
@Ayanokouji 那就是在最底层的那个 err 打印吧
|
12
Ayanokouji OP @dylanqqt 嗯,就是最底层其实不是那么好界定,写法也比较啰嗦。不过这样的日志确实能当调用栈串联起来
|
13
bli22ard 3 天前
@Ayanokouji github.com/pkg/errors 这个库还能再战。其实自己定义也可以,实现 error 的 struct ,增加一个 Wrap 函数,参数接受一个 error ,函数体,runtime 拿调用栈,将结果保存在 struct 的成员变量,这样就可以用了
|
14
qW7bo2FbzbC0 3 天前
```go
err := doJob() if err != nil { return fmt.Errorf("do job failed, %w", err) } ``` |
15
Ayanokouji OP @qW7bo2FbzbC0 我目前就是这种做法,这种做法的缺陷是,前缀的 message 需要足够清晰和唯一,清晰是为了可读,唯一是为了代码搜索定位当调用栈来用。
还有如果项目做国际化,这种需要定义错误吗,维护比较费精力。 |
16
lbp0200 3 天前
这个和语言无关
错误的地方记录日志,包括当前行,当前文件 上级代码只需要知道有错误就行 |
17
zacard 3 天前
和 java 的异常处理类似,底层库/函数用 errors 包装携带堆栈,上层统一捕获 error 打印即可。github.com/pkg/errors 虽然不更新了,但是够用了
|
18
guanzhangzhang 3 天前
两种,一种是 wrap 一下信息,我博客很多文章搜报错能直接搜到开源项目源码里去,另一种是 zap 这种 logger 能打印带文件 go:行数的
|
19
Kauruus 3 天前
你在 error 里带上调用栈也不是不行(例如 https://github.com/go-errors/errors ),然后在顶层处理,打印或者发到 sentry 。
|
20
z1829909 3 天前 1
我一般也是每层返回的时候包一些关键字, 相当于人肉造了一个栈.
|
21
xxlxiaxiaolei 3 天前
@guanzhangzhang 真张馆长?我还有你 QQ 呢
|
22
Linxing 3 天前
WithStack
|
23
pkoukk 3 天前
一般我不会携带堆栈信息,太多了,去获取当前堆栈的资源消耗也太重了。
其实很简单啊,error 是个 interface ,return error 的时候变成 retrun MyError(code,err)就行了 额外附加一个 code 足矣. 这是个简单的示例,实际上还可以做很多事情在里面 func MyError(code int, err error) error { if errors.As(err, &myError{}) { return err } else { return &myError{code: code, source: err} } } |
24
lifei6671 3 天前
@Ayanokouji #2 golang 的 1.21 可以直接用%w 来包裹 error ,也可以用 errors.join 来合并多个 error 。不需要第三方库了。
|
25
pkoukk 3 天前
对你的 APPEND 的回复:
那你直接用 panic ,那玩意里面自带堆栈,去上游 revocer 。 喜欢 try catch 的,用 panic recover 去 |
26
Nazz 3 天前
我的做法是用 github.com/pkg/errors 带上错误堆栈, 用 zerolog 设置错误级别, 在响应函数里面写入日志, lumberjack 做日志切分
|
27
Ayanokouji OP @pkoukk 你是二极管吗,要看调用栈就得用 panic 啊,你家写 go 全是 panic 啊。前面写那么多看了吗,还是说你写过 go 吗。有能耐把你的解决方法写出来啊。
|
28
Vitumoc 3 天前
没有特别看懂 OP 想要表达什么。
如果只是想要调用栈的话,并不困难啊? 大概就这样: ```go import "runtime/debug" func main() { defer func() { stack := debug.Stack() fmt.Sprintf("调用栈信息:\n%s", stack) } } ``` 实际用的时候再做一些封装 |
29
Ayanokouji OP @Vitumoc 不是非必要用调用栈,一切为了问题排查,就比如日志突兀的出现一句:Error: unexpected '>' at the beginning of value 。这种情况知道是 json 错误,但是哪里发生的 json 错误,参数是什么之类的,定位解决问题的时候,很困难
|
30
pkoukk 3 天前
@Ayanokouji #27 我写了,你没看,你才是二极管
|
31
pkoukk 3 天前
@Ayanokouji #27 这么简单的解法都想不出来,怀疑你的水平
|
32
Ayanokouji OP @pkoukk 所以你家 go ,全是 panic recover ? java 的 try catch = go 的 panic recover ?
|
33
p1gd0g 3 天前
没太看懂。
我们这边错误码都是封装好的,每次使用都是 new 出来的,自带简略堆栈信息和代码行号。在返回上层的时候就不再处理 error 了,也没必要。第三方的错误码转掉,保证返回网关时错误码都是同一套。线上再搭配链路追踪,查问题足够了。 不知道能不能解决你的问题。 依稀记得官方库已经支持 error wrap 了所以不再需要 github.com/pkg/errors ,没细看过不保真。 |
34
Kumo31 3 天前 3
可以看下这个库: https://github.com/samber/oops
|
35
qW7bo2FbzbC0 3 天前
@Ayanokouji 在同一种错误需要多次使用的情况下,我也是参考开源项目,对错误进行实例化,然后在项目内返回这个实例化的错误。至于国际化,我不太清楚,我尽量都用英文定义错误和打印日志
|
36
Ayanokouji OP @p1gd0g 官方只支持了 %w ,错误码封装的思路,需要自定义 error 。应该和 huma 的思路类似
https://github.com/danielgtaylor/huma/blob/main/error.go |
37
zoharSoul 3 天前
很多人没写过那种复杂业务, 他理解不了这些场景
|
38
tiedan 3 天前
只要遇到就应该打印日志
|
39
p1gd0g 3 天前
|
40
aababc 3 天前
同样的问题,我也问过,怎么说呢,感觉 golang 这边没有啥统一的做法,看了看大佬意思就是 error 是一个 interface ,你自己想咋封装都可以
|
41
guanzhangzhang 3 天前
|
42
dwu8555 3 天前
Panic 没什么错,Erlang 有一个思想:“Let it crash”, 就是要让程序 Crash 掉,不要隐藏错误。
|
43
securityCoding 3 天前
具体业务代码主动抛 err 需要前置 log.Errorf 打印出来,如果是调用方则无脑 return 即可
|
45
aababc 3 天前
@tiedan #38 @harleyliao #4 这样真的好吗,我总的感觉就是 要么返回一个 error ,要么记录一个 error ,不要两件事都干
|
46
Ayanokouji OP @dwu8555 嗯。。。这么干,饭都没得吃了
|
47
frank000 3 天前
我现在用 zap, 只要遇到 err 错误就打 log ,同时 zap.Error(err)打印,然后返给上层错误。 每层都需要打印 log 和返回错误,一般也不会很多层 。
|
48
Ayanokouji OP @aababc error 可以自定义,java 也有自定义的 excetion ,这样做的目的为了统一错误处理。
可以参考 https://github.com/danielgtaylor/huma/blob/main/error.go 但是吧,即使自定义 error ,如果 error 不带堆栈,仅靠 error 大概率还是无法确定错误位置,还是得靠 error + log 来解决。 |
49
aababc 3 天前
@Ayanokouji #48 怎么说呢,这就是 go 的特点吧,我们现在的做法就是每层都追加自己的信息,相当于手工造了一个堆栈
|
50
Ayanokouji OP @aababc 是的,我目前也是这么解决的 return fmt.Errorf("xxx:%w", err),其实自定义 error 和 fmt.Errorf 的区别,就是看后续是否需要针对 error 类型细化处理(一般是中间件之类的)。
|
51
NotLongNil 3 天前
@Kumo31 这个好,我也在在找类似的库,收藏了。这个能完美满足楼主的需求
|
52
nextvay 3 天前
1. 每次返回 err 都 errors.wrap 包一下
2. 再想打印日志的地方,记录 堆栈日志 :fmt.Sprintf("%+v", err) |
53
windcode 3 天前
我的做法是:
1. API/方法返回的 error ,包含错误码和层层 warp 的错误信息( ErrCode 和 Message ),但是不包含错误堆栈 2. 服务端在中间件层统一打印错误堆栈 核心思路是对外的接口是给用户看的,透出的应该是可读性较好、经过抽象的信息,而错误堆栈是给开发者看的,排查问题用的,所以放在日志里。 |
54
lwldcr 3 天前
|
56
blur1119 3 天前
最近看最佳实践这词看吐了
|
57
dingyaguang117 3 天前 via iPhone
pkg/errors 不是标准做法嘛 😂😂
|
58
zhu327808 2 天前
|
59
SingeeKing 2 天前 via iPhone 1
自荐一下我自己基于 pkg/errors 改的 ee 库
以下摘自我的博客: 最优的方案实际上是全局使用第三方库。这里推荐使用我自己的 ee 错误处理库( https://pkg.go.dev/github.com/ImSingee/go-ex/ee ),其修改自官方的 pkg/errors 库,但基于实际需求做了一定的优化: 1. (相比标准库)为所有的错误都包装了调用栈信息。 2. 对于已经存在调用栈信息的,不会覆盖(来保证永远可以拿到最深层的调用栈信息)。 3. 支持在 WithStack 时指定 skip 来使用上层栈(用于编写工具函数)。 4. 栈信息的 StackTrace 和 Frame 可访问,以供外部工具(例如日志处理库)结构化利用。 5. 增加 Panic 函数,调用时会自动生成 error 并记录 panic 位置信息。 6. 所有 error 都实现了 TextMarshaler 接口,对序列化友好。 |
60
oaix 2 天前
|
61
DefoliationM 2 天前
promtail + loki + otel collector + prometheus + grafana 请,fmt.Error("xxx: %w",err) 已经足够了,你最后都能找到是哪里报的错。不过你既然习惯 java 的 throw try catch ,我建议还是继续使用 java 比较好,没必要强行转 go ,java 的性能不比 go 差,生态也比 go 好,完全没必要转 go 。
|
62
BeautifulSoap 2 天前 via Android
就用 github.com/pkg/errors 这个包一层层往上套娃啊
这个包虽然已经不维护了,但依旧是现在实际 go 项目的错误处理中的标准做法之一,你直接用就是了 而且这个包里的代码内容简单得一批,你真遇到问题想改的话直接自己本地创建个文件,复制粘贴一份直接改就行了 |
63
aarontian 2 天前
两年没写 go 了,我当时的做法是封装了个自己的 errorx 包和自定义的 Error 接口,模仿 throw 的做法,在里面封装好 throw 时的调用栈,以及预定义的错误码
|
64
zjsxwc 2 天前 via Android
要不模拟 rust 的处理方式,
rust 是用 Result<OK, Err>,配合问号后缀语法糖来解决的, 所以可以首先用 https://github.com/Boyux/go_macro 让 go 能有类似 rust 的问号后缀语法糖,简化判断 is err 的处理, 然后在 go 代码里模拟 Result<OK, Err>,就行了,比如 // 定义 Result 类型,它有两个类型参数,一个表示成功的值类型,一个表示错误类型 type Result[T any, E error] struct { value T err E } // Ok 构造函数,用于创建表示成功的 Result 实例 func Ok[T any, E error](v T) Result[T, E] { return Result[T, E]{ value: v, err: nil, } } // Err 构造函数,用于创建表示失败(有错误)的 Result 实例 func Err[T any, E error](e E) Result[T, E] { return Result[T, E]{ value: *new(T), err: e, } } // IsOk 方法判断 Result 是否是成功状态 func (r Result[T, E]) IsOk() bool { return r.err == nil } // IsErr 方法判断 Result 是否是错误状态 func (r Result[T, E]) IsErr() bool { return r.err!= nil } // Unwrap 方法,如果是成功状态则返回值,若是错误状态则触发 panic (类似 Rust 中直接使用.操作符获取值但不处理错误的情况) func (r Result[T, E]) Unwrap() T { if r.IsErr() { panic(r.err) } return r.value } // UnwrapErr 方法,如果是错误状态则返回错误,否则返回 nil func (r Result[T, E]) UnwrapErr() E { return r.err } func divide(a, b int) Result[int, error] { if b == 0 { return Err[int, error](fmt.Errorf("division by zero")) } return Ok[int, error](a / b) } func main() { result := divide(10, 2) if result.IsOk() { fmt.Println("Result:", result.Unwrap()) } else { fmt.Println("Error:", result.UnwrapErr()) } } |
65
henix 2 天前 1
我用了 Go 的错误处理后有个感受:调用栈真不是必需的
说起调用栈我就想起网传的这张图: https://www.cnblogs.com/jhj117/p/5627224.html 那么多调用栈全是中间层的,对排查错误也没啥帮助 但题目中的这种情况属于信息过少,也无法很好排错 那怎么办 我认为很多时候我们需要的不是调用栈,而是错误的上下文 比如读写文件错误的时候的文件名、请求上游 API 错误的时候的 url 而这些都不是简单的一个调用栈能自动解决的,都需要程序员在错误发生的附近手动添加 在错误向上传递的过程中,如果哪层有很重要的上下文,就在那一层把相关信息加到 err 里 Error: unexpected '>' at the beginning of value 这种错误,应该把参数名和值都输出出来,并且当 err 传递到 controller 层的时候,附加上请求信息 |
67
iyaozhen 2 天前
楼主说的一点没错,这就是 go 的问题。而且业界也没达成统一(比如要不要堆栈)
我们公司内部也很乱,基本上一个团队一个做法。楼主自己定一个就行。 目前比较推荐的做法是,自定义 error ,然后 适度包一下 fmt.Errorf("xxx:%w", err) 调用方通过 errors.is 判断类型做业务逻辑处理 但话说回来,go 设计上就是互联网的 c ,没有那么多特性。特别是不要用 java 的思维理解 go ,不然也是自己痛苦 |
68
freestyle 2 天前 via iPhone
|
69
linuxsuren 2 天前
https://github.com/LinuxSuRen/api-testing 完全开源的接口开发、测试工具
|
70
kivmi 2 天前
func ErrWrap(err error, message string) (e error) {
if err != nil { fmt.Println(fmt.Errorf("Error: %v\nStack trace:\n%s", err, debug.Stack())) slog.Info(message) return err } return nil } func covert(data string) (result map[string]interface{}, err error) { e := json.Unmarshal([]byte(data), &result) e = ErrWrap(e, "Json 解析错误") return result, e } 类似这样的,是否满足你的需求呢? |
71
kivmi 2 天前
其实 github.com/gookit/slog 中已经有了所有的信息,包括行信息,当然这种情况下,对于多个链路调用没那么友好,只能看到发生错误的地方,到底是哪个模块产生的错误,还是不是很清楚,因此可以打印整个的调用栈帧,如下:
func printCallers() { var pcs [10]uintptr n := runtime.Callers(2, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) for { frame, more := frames.Next() fmt.Printf("Function: %s\nFile: %s\nLine: %d\n\n", frame.Function, frame.File, frame.Line) if !more { break } } } func ErrWrap(err error, message string) (e error) { if err != nil { slog.Info(message) printCallers() return err } return nil } 这样既可以拿到对应的行,也可以看到整个的调用栈: [2025/01/01T15:45:43.828] [application] [INFO] [main.go:30,ErrWrap] Json 解析错误 Function: main.ErrWrap File: F:/workspace/go/errors-demo/main.go Line: 31 Function: main.covert File: F:/workspace/go/errors-demo/main.go Line: 39 Function: main.main File: F:/workspace/go/errors-demo/main.go Line: 51 Function: runtime.main File: C:/Program Files/Go/src/runtime/proc.go Line: 250 Function: runtime.goexit File: C:/Program Files/Go/src/runtime/asm_amd64.s Line: 1594 Error: invalid character '>' looking for beginning of value Result: map[] Process finished with the exit code 0 |
72
kuanat 1 天前
|
73
kuanat 1 天前
关于 Go 日志的话题之前也有过几个帖子,可以参考一下,恰好我在那几个帖子里也有论述一些观点做法。
如何更好的打印日志 https://v2ex.com/t/1043663 golang 日志记录 https://v2ex.com/t/1038327 Golang 中的 Context 为什么只有上文没有下文?一般如何传递下文? https://v2ex.com/t/1012453 回到这个帖子的重点,关于“定位代码出错位置”这个需求,需要先明确调用栈的定义。除了代码层面的 call stack ,业务逻辑上 trace 也可以叫作调用栈。 从 OP 的描述来看,主要矛盾是业务流程上比较长,日志中间件的报错不足以定位特定模块代码层面 call stack 的问题。 我在上面引用的第一个帖子里提到过一些笼统的解决思路。 帖子里我提到的 debug/release 双版本具体实现是用 build tags 做一个开关,release 版本没有任何额外输出,debug 版本会输出 code path 的相关信息。或者理解成单元测试 coverage 的做法。这样不仅可以知道当前模块的输入、输出,也知道具体代码的分支路径。 这个做法给我节省了大量 debug 的时间,之前经常需要单步看执行逻辑,现在基本上看下分支流程就能大致定位问题了。并不是一定要通过反射或者什么方式获得出错的代码行才叫定位。 |
74
Ayanokouji OP @henix #65 认同这个观点,调用栈属于语言或者框架层面的保底机制。有了上下文也可以快速帮助排错。Error: unexpected '>' at the beginning of value 这种错误,仅仅用一个 fmt.Errorf("xxx:%w",err),也不太好处理,需要结合日志或者自定义错误类型处理
|
75
xyqhkr 1 天前
要点:
1. 只在入口处打印一次错误日志。其它地方绝对不打印错误。 解决调用位置方法有两个: 1. 在 return 处 使用 Wrap 包装。 2. 在第一次 err 处 new CustomErr 结构。 |
76
ForkNMB 1 天前
这还需要三方库? 自己写点代码美化一下输出就好吧 用 syslog 为例,syslog.New(syslog.LOG_LOCAL0, "XXX") 包装一下常用的 Info Error Debug 方法 写出去写统一 format 一下。
至于函数名 行号 堆栈这些,简单用 pc, file, line, _ := runtime.Caller(n) n 具体数字取决于你的封装 。堆栈可以等有 Panic 时再处理打印出来 平时定位 error 也不需要像 java 那样打印堆栈吧。遇到 error 打印,那肯定是根据实际情况有些是必须打的,有些可以合并处理在上层补充就行。 |
78
qloog 23 小时 50 分钟前 1
使用 github.com/pkg/errors
1.业务最底层,比如 db,api, rpc 等等,使用 errors.Wrap(...) - 携带堆栈 2.中间层,errors.WithMessage(err, "your custom msg...") - 携带本层的自定义信息 3.最上层打印错误日志,log.Errorf("xxxxx, err: %w", error) - 打印日志 PS: 中间使用 errors.WithMessage 而不是 errors.Wrap ,是未了避免最上层打印太多的堆栈信息,只在最底层携带一次堆栈信息 |
79
ikaros 21 小时 59 分钟前
以前用 logrus 的时候有个参数可以打印出代码具体位置,行信息,可以看下是怎么实现的
|
81
Ayanokouji OP @ikaros 在错误的位置打印日志的话,不要调用栈也没关系,这样相当于自定义上下文
|