求教: golang error 和 log 的最佳实践思路

272 天前
 Ayanokouji
用 go 写业务时,遇到 error 时,大多数情况只能一路向上 return err ,我们基于这个场景讨论。

这个场景,和 java 写业务遇到的 checked exception 很类似,写代码时,只能一路向上 throw (或者 catch 住 throw new unchecked exception ),最终由框架统一处理。

如果遇到 go 遇到经验不足的开发者(比如以前的我),就会看到这样的错误日志:

Error: unexpected '>' at the beginning of value

Error: EOF

Error: wrong argument value

嗯。。。这 tm 是啥啊,到底是哪一行代码出错啊。

调用链越长,问题越难排查。

比较通用的 web 业务调用链,一般是 handler -> service -> 中间件(数据库/redis/第三方 api 等)

随着坑踩的多了,现在遇到 err, 一般是 return fmt.Errorf("xxx:%w", err)

日志一般在 handler 层 slog.log("xxx", slog.Any("request", req), slog.Any("err", err))

但是缺少了调用栈,总觉得少了点什么。

请教下各位,如何平衡 error 和 log ,主要是服务于问题排查。

看过 echo 和 huma 的框架 error 处理,都是自定义 err ,框架统一处理

------

ps:那些上来只踩一脚 java 的,还是去多写写代码吧,这种 err ( unexpected '>' at the beginning of value ) 真的比 excetiop (含调用栈) 强吗。
6205 次点击
所在节点    Go 编程语言
81 条回复
DefoliationM
271 天前
promtail + loki + otel collector + prometheus + grafana 请,fmt.Error("xxx: %w",err) 已经足够了,你最后都能找到是哪里报的错。不过你既然习惯 java 的 throw try catch ,我建议还是继续使用 java 比较好,没必要强行转 go ,java 的性能不比 go 差,生态也比 go 好,完全没必要转 go 。
BeautifulSoap
271 天前
就用 github.com/pkg/errors 这个包一层层往上套娃啊
这个包虽然已经不维护了,但依旧是现在实际 go 项目的错误处理中的标准做法之一,你直接用就是了
而且这个包里的代码内容简单得一批,你真遇到问题想改的话直接自己本地创建个文件,复制粘贴一份直接改就行了
aarontian
271 天前
两年没写 go 了,我当时的做法是封装了个自己的 errorx 包和自定义的 Error 接口,模仿 throw 的做法,在里面封装好 throw 时的调用栈,以及预定义的错误码
zjsxwc
271 天前
要不模拟 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())
}
}
henix
271 天前
我用了 Go 的错误处理后有个感受:调用栈真不是必需的
说起调用栈我就想起网传的这张图: https://www.cnblogs.com/jhj117/p/5627224.html
那么多调用栈全是中间层的,对排查错误也没啥帮助

但题目中的这种情况属于信息过少,也无法很好排错
那怎么办
我认为很多时候我们需要的不是调用栈,而是错误的上下文
比如读写文件错误的时候的文件名、请求上游 API 错误的时候的 url
而这些都不是简单的一个调用栈能自动解决的,都需要程序员在错误发生的附近手动添加
在错误向上传递的过程中,如果哪层有很重要的上下文,就在那一层把相关信息加到 err 里

Error: unexpected '>' at the beginning of value 这种错误,应该把参数名和值都输出出来,并且当 err 传递到 controller 层的时候,附加上请求信息
cooooing
271 天前
@henix 好生草的图,不过确实排查错误需要的是上下文而不是调用栈。
iyaozhen
271 天前
楼主说的一点没错,这就是 go 的问题。而且业界也没达成统一(比如要不要堆栈)

我们公司内部也很乱,基本上一个团队一个做法。楼主自己定一个就行。 目前比较推荐的做法是,自定义 error ,然后 适度包一下 fmt.Errorf("xxx:%w", err)
调用方通过 errors.is 判断类型做业务逻辑处理

但话说回来,go 设计上就是互联网的 c ,没有那么多特性。特别是不要用 java 的思维理解 go ,不然也是自己痛苦
freestyle
271 天前
linuxsuren
271 天前
https://github.com/LinuxSuRen/api-testing 完全开源的接口开发、测试工具
kivmi
271 天前
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
} 类似这样的,是否满足你的需求呢?
kivmi
271 天前
其实 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
kuanat
270 天前
关于 Go 日志的话题之前也有过几个帖子,可以参考一下,恰好我在那几个帖子里也有论述一些观点做法。

如何更好的打印日志 https://ex.noerr.eu.org/t/1043663
kuanat
270 天前
关于 Go 日志的话题之前也有过几个帖子,可以参考一下,恰好我在那几个帖子里也有论述一些观点做法。

如何更好的打印日志 https://ex.noerr.eu.org/t/1043663
golang 日志记录 https://ex.noerr.eu.org/t/1038327
Golang 中的 Context 为什么只有上文没有下文?一般如何传递下文? https://ex.noerr.eu.org/t/1012453


回到这个帖子的重点,关于“定位代码出错位置”这个需求,需要先明确调用栈的定义。除了代码层面的 call stack ,业务逻辑上 trace 也可以叫作调用栈。

从 OP 的描述来看,主要矛盾是业务流程上比较长,日志中间件的报错不足以定位特定模块代码层面 call stack 的问题。

我在上面引用的第一个帖子里提到过一些笼统的解决思路。

帖子里我提到的 debug/release 双版本具体实现是用 build tags 做一个开关,release 版本没有任何额外输出,debug 版本会输出 code path 的相关信息。或者理解成单元测试 coverage 的做法。这样不仅可以知道当前模块的输入、输出,也知道具体代码的分支路径。

这个做法给我节省了大量 debug 的时间,之前经常需要单步看执行逻辑,现在基本上看下分支流程就能大致定位问题了。并不是一定要通过反射或者什么方式获得出错的代码行才叫定位。
Ayanokouji
270 天前
@henix #65 认同这个观点,调用栈属于语言或者框架层面的保底机制。有了上下文也可以快速帮助排错。Error: unexpected '>' at the beginning of value 这种错误,仅仅用一个 fmt.Errorf("xxx:%w",err),也不太好处理,需要结合日志或者自定义错误类型处理
xyqhkr
270 天前
要点:
1. 只在入口处打印一次错误日志。其它地方绝对不打印错误。
解决调用位置方法有两个:
1. 在 return 处 使用 Wrap 包装。
2. 在第一次 err 处 new CustomErr 结构。
ForkNMB
270 天前
这还需要三方库? 自己写点代码美化一下输出就好吧 用 syslog 为例,syslog.New(syslog.LOG_LOCAL0, "XXX") 包装一下常用的 Info Error Debug 方法 写出去写统一 format 一下。
至于函数名 行号 堆栈这些,简单用 pc, file, line, _ := runtime.Caller(n) n 具体数字取决于你的封装 。堆栈可以等有 Panic 时再处理打印出来 平时定位 error 也不需要像 java 那样打印堆栈吧。遇到 error 打印,那肯定是根据实际情况有些是必须打的,有些可以合并处理在上层补充就行。
alexliux
270 天前
@henix 是的,我也一直在给团队强调要把上下文打印出来,不要干瘪瘪的只有一个错误。
qloog
270 天前
使用 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 ,是未了避免最上层打印太多的堆栈信息,只在最底层携带一次堆栈信息
ikaros
270 天前
以前用 logrus 的时候有个参数可以打印出代码具体位置,行信息,可以看下是怎么实现的
maladaxia
269 天前
@zacard 正解, golang error 也可以打印栈的

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

https://ex.noerr.eu.org/t/1101542

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

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

© 2021 V2EX