我对Go语言的错误处理有哪些不满,以及我是如何处理的

写 Go
的人往往对它的错误处理模式有一定的看法。按不同的语言经验,人们可能有不同的习惯处理方法。这就是为什么我决定要写这篇文章,尽管有点固执己见,但我认为听取我的经验是有用的。我想要讲的主要问题是,很难去强制执行良好的错误处理实践,错误经常没有堆栈追踪,并且错误处理本身太冗长。不过,我已经看到了一些潜在的解决方案,或许能帮助解决一些问题。

与其他语言的快速比较

在 Go 中,所有的错误都是值。因为这点,相当多的函数最后会返回一个 error, 看起来像这样:


  1. func (s *SomeStruct) Function() (string, error) 

因此这导致调用代码通常会使用 if 语句来检查它们:


  1. bytes, err := someStruct.Function()
  2. if err != nil {
  3. // Process error
  4. }

另外一种方法,是在其他语言中,如 Java、C#、Javascript、Objective C、Python 等使用的 try-catch 模式。如下你可以看到与先前的 Go 示例类似的 Java 代码,声明 throws 而不是返回 error:


  1. public String function() throws Exception

它使用的是 try-catch 而不是 if err != nil:


  1. try {
  2. String result = someObject.function()
  3. // continue logic
  4. }
  5. catch (Exception e) {
  6. // process exception
  7. }

当然,还有其他的不同。例如,error 不会使你的程序崩溃,然而 Exception 会。还有其他的一些,在本篇中会专门提到这些。

实现集中式错误处理

退一步,让我们看看为什么要在一个集中的地方处理错误,以及如何做到。

大多数人或许会熟悉的一个例子是 web 服务 - 如果出现了一些未预料的的服务端错误,我们会生成一个 5xx 错误。在 Go 中,你或许会这么实现:


  1. func init() {
  2. http.HandleFunc("/users", viewUsers)
  3. http.HandleFunc("/companies", viewCompanies)
  4. }
  5.  
  6. func viewUsers(w http.ResponseWriter, r *http.Request) {
  7. user // some code
  8. if err := userTemplate.Execute(w, user); err != nil {
  9. http.Error(w, err.Error(), 500)
  10. }
  11. }
  12.  
  13. func viewCompanies(w http.ResponseWriter, r *http.Request) {
  14. companies = // some code
  15. if err := companiesTemplate.Execute(w, companies); err != nil {
  16. http.Error(w, err.Error(), 500)
  17. }
  18. }

这并不是一个好的解决方案,因为我们不得不重复地在所有的处理函数中处理错误。为了能更好地维护,最好能在一处地方处理错误。幸运的是,在 Go
语言的官方博客中,Andrew Gerrand 提供了一个替代方法,可以完美地实现。我们可以创建一个处理错误的 Type:


  1. type appHandler func(http.ResponseWriter, *http.Request) error
  2.  
  3. func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  4. if err := fn(w, r); err != nil {
  5. http.Error(w, err.Error(), 500)
  6. }
  7. }

这可以作为一个封装器来修饰我们的处理函数:


  1. func init() {
  2. http.Handle("/users", appHandler(viewUsers))
  3. http.Handle("/companies", appHandler(viewCompanies))
  4. }

接着我们需要做的是修改处理函数的签名来使它们返回 errors。这个方法很好,因为我们做到了 DRY 原则,并且没有重复使用不必要的代码 - 现在我们可以在单独一个地方返回默认错误了。

错误上下文

在先前的例子中,我们可能会收到许多潜在的错误,它们中的任何一个都可能在调用堆栈的许多环节中生成。这时候事情就变得棘手了。

为了演示这点,我们可以扩展我们的处理函数。它可能看上去像这样,因为模板执行并不是唯一一处会发生错误的地方:


  1. func viewUsers(w http.ResponseWriter, r *http.Request) error {
  2. user, err := findUser(r.formValue("id"))
  3. if err != nil {
  4. return err;
  5. }
  6. return userTemplate.Execute(w, user);
  7. }

调用链可能会相当深,在整个过程中,各种错误可能在不同的地方实例化。Russ Cox的这篇文章解释了如何避免遇到太多这类问题的最佳实践:

“在 Go 中错误报告的部分约定是函数包含相关的上下文,包括正在尝试的操作(比如函数名和它的参数)。”

这个给出的例子是对 OS 包的一个调用:


  1. err := os.Remove("/tmp/nonexist")
  2. fmt.Println(err)

它会输出:


  1. remove /tmp/nonexist: no such file or directory

总结一下,执行后,输出的是被调用的函数、给定的参数、特定的出错信息。当在其他语言中创建一个 Exception 消息时,你也可以遵循这个实践。如果我们在 viewUsers 处理中坚持这点,那么几乎总是能明确错误的原因。

问题来自于那些不遵循这个最佳实践的人,并且你经常会在第三方的 Go 库中看到这些消息:


  1. Oh no I broke

这没什么帮助 - 你无法了解上下文,这使得调试很困难。更糟糕的是,当这些错误被忽略或返回时,这些错误会被备份到堆栈中,直到它们被处理为止:


  1. if err != nil {
  2. return err
  3. }

这意味着错误何时发生并没有被传递出来。

应该注意的是,所有这些错误都可以在 Exception 驱动的模型中发生 - 糟糕的错误信息、隐藏异常等。那么为什么我认为该模型更有用?

即便我们在处理一个糟糕的异常消息,我们仍然能够了解它发生在调用堆栈中什么地方。因为堆栈跟踪,这引发了一些我对 Go 不了解的部分 -
你知道 Go 的 panic 包含了堆栈追踪,但是 error 没有。我推测可能是 panic
会使你的程序崩溃,因此需要一个堆栈追踪,而处理错误并不会,因为它会假定你在它发生的地方做一些事。

所以让我们回到之前的例子 - 一个有糟糕错误信息的第三方库,它只是输出了调用链。你认为调试会更容易吗?


  1. panic: Oh no I broke
  2. [signal 0xb code=0x1 addr=0x0 pc=0xfc90f]
  3.  
  4. goroutine 1103 [running]:
  5. panic(0x4bed00, 0xc82000c0b0)
  6. /usr/local/go/src/runtime/panic.go:481 +0x3e6
  7. github.com/Org/app/core.(_app).captureRequest(0xc820163340, 0x0, 0x55bd50, 0x0, 0x0)
  8. /home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:313 +0x12cf
  9. github.com/Org/app/core.(_app).processRequest(0xc820163340, 0xc82064e1c0, 0xc82002aab8, 0x1)
  10. /home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:203 +0xb6
  11. github.com/Org/app/core.NewProxy.func2(0xc82064e1c0, 0xc820bb2000, 0xc820bb2000, 0x1)
  12. /home/ubuntu/.go_workspace/src/github.com/Org/App/core/proxy.go:51 +0x2a
  13. github.com/Org/app/core/vendor/github.com/rusenask/goproxy.FuncReqHandler.Handle(0xc820da36e0, 0xc82064e1c0, 0xc820bb2000, 0xc5001, 0xc820b4a0a0)
  14. /home/ubuntu/.go_workspace/src/github.com/Org/app/core/vendor/github.com/rusenask/goproxy/actions.go:19 +0x30

我认为这可能是 Go 的设计中被忽略的东西 - 不是所有语言都不会忽视的。

如果我们使用 Java 作为一个随意的例子,其中人们犯的一个最愚蠢的错误是不记录堆栈追踪:


  1. LOGGER.error(ex.getMessage()) // 不记录堆栈追踪
  2. LOGGER.error(ex.getMessage(), ex) // 记录堆栈追踪

但是 Go 似乎在设计中就没有这个信息。

在获取上下文信息方面 - Russ 还提到了社区正在讨论一些潜在的接口用于剥离上下文错误。关于这点,了解更多或许会很有趣。

堆栈追踪问题解决方案

幸运的是,在做了一些查找后,我发现了这个出色的 Go 错误库来帮助解决这个问题,来给错误添加堆栈跟踪:


  1. if errors.Is(err, crashy.Crashed) {
  2. fmt.Println(err.(*errors.Error).ErrorStack())
  3. }

不过,我认为这个功能如果能成为语言的第一类公民first class citizenship将是一个改进,这样你就不必做一些类型修改了。此外,如果我们像先前的例子那样使用第三方库,它可能没有使用 crashy - 我们仍有相同的问题。

我们对错误应该做什么?

我们还必须考虑发生错误时应该发生什么。这一定有用,它们不会让你的程序崩溃,通常也会立即处理它们:


  1. err := method()
  2. if err != nil {
  3. // some logic that I must do now in the event of an error!
  4. }

如果我们想要调用大量方法,它们会产生错误,然后在一个地方处理所有错误,这时会发生什么?看上去像这样:


  1. err := doSomething()
  2. if err != nil {
  3. // handle the error here
  4. }
  5.  
  6. func doSomething() error {
  7. err := someMethod()
  8. if err != nil {
  9. return err
  10. }
  11. err = someOther()
  12. if err != nil {
  13. return err
  14. }
  15. someOtherMethod()
  16. }

这感觉有点冗余,在其他语言中你可以将多条语句作为一个整体处理。


  1. try {
  2. someMethod()
  3. someOther()
  4. someOtherMethod()
  5. }
  6. catch (Exception e) {
  7. // process exception
  8. }

或者只要在方法签名中传递错误:


  1. public void doSomething() throws SomeErrorToPropogate {
  2. someMethod()
  3. someOther()
  4. someOtherMethod()
  5. }

我个人认为这两个例子实现了一件事情,只是 Exception 模式更少冗余,更加弹性。如果有什么的话,我觉得 if err!= nil 感觉像样板。也许有一种方法可以清理?

将失败的多条语句做为一个整体处理错误

首先,我做了更多的阅读,并在 Rob Pike 写的 Go 博客中发现了一个比较务实的解决方案。

他定义了一个封装了错误的方法的结构体:


  1. type errWriter struct {
  2. w io.Writer
  3. err error
  4. }
  5.  
  6. func (ew *errWriter) write(buf []byte) {
  7. if ew.err != nil {
  8. return
  9. }
  10. _, ew.err = ew.w.Write(buf)
  11. }

让我们这么做:


  1. ew := &errWriter{w: fd}
  2. ew.write(p0[a:b])
  3. ew.write(p1[c:d])
  4. ew.write(p2[e:f])
  5. // and so on
  6. if ew.err != nil {
  7. return ew.err
  8. }

这也是一个很好的方案,但是我感觉缺少了点什么 - 因为我们不能重复使用这个模式。如果我们想要一个含有字符串参数的方法,我们就不得不改变函数签名。或者如果我们不想执行写操作会怎样?我们可以尝试使它更通用:


  1. type errWrapper struct {
  2. err error
  3. }

  1. func (ew *errWrapper) do(f func() error) {
  2. if ew.err != nil {
  3. return
  4. }
  5. ew.err = f();
  6. }

但是我们有一个相同的问题,如果我们想要调用含有不同参数的函数,它就无法编译了。然而你可以简单地封装这些函数调用:


  1. w := &errWrapper{}
  2.  
  3. w.do(func() error {
  4. return someFunction(1, 2);
  5. })
  6.  
  7. w.do(func() error {
  8. return otherFunction("foo");
  9. })
  10.  
  11. err := w.err
  12.  
  13. if err != nil {
  14. // process error here
  15. }

这可以用,但是并没有太大帮助,因为它最终比标准的 if err != nil 检查带来了更多的冗余。如果有人能提供其他解决方案,我会很有兴趣听。或许这个语言本身需要一些方法来以不那么臃肿的方式传递或者组合错误 - 但是感觉似乎是特意设计成不那么做。

总结

看完这些之后,你可能会认为我在对 error 挑刺儿,由此推论我反对 Go。事实并非如此,我只是将它与我使用 try catch
模型的经验进行比较。它是一个用于系统编程很好的语言,并且已经出现了一些优秀的工具。仅举几例,有
Kubernetes、Docker、Terraform、Hoverfly 等。还有小型、高性能、本地二进制的优点。但是,error 难以适应。
我希望我的推论是有道理的,而且一些方案和解决方法可能会有帮助。

本文作者:Andrew Morgan

来源:51CTO

时间: 2024-11-09 23:57:59

我对Go语言的错误处理有哪些不满,以及我是如何处理的的相关文章

Go语言中错误处理实例分析_Golang

本文实例讲述了Go语言中错误处理的方法.分享给大家供大家参考.具体分析如下: 错误是可以用字符串描述自己的任何东西. 主要思路是由预定义的内建接口类型 error,和其返回返回字符串窜的方法 Error 构成. type error interface { Error() string } 当用 fmt 包的多种不同的打印函数输出一个 error 时,会自动的调用该方法. 复制代码 代码如下: package main import (     "fmt"     "time

gcc-在GCC上的C语言编程错误问题

问题描述 在GCC上的C语言编程错误问题 #include #include #include struct LinkList { int data; struct LinkList next; }; struct LinkList Reverse ( LinkList *list ) *出错** { LNode *tmp = NULL; LNode *p = NULL; if (list == NULL) { return NULL; } tmp = list->next; while (tm

c语言-关于UBUNTU下C语言 段错误 转存储的问题

问题描述 关于UBUNTU下C语言 段错误 转存储的问题 在Linux下雪C语言编程时, 1.捕捉文件操作错误并打印错误信息:strerror(errno) 2.用域名取得ip地址是打印IP是调用:inet_ntoa()时 总是出现段错误 ,该如何解决! 解决方案 确认你定义了两个类似的结构体实例: struct sockaddr_in client socklen_t length; length=sizeof(client); 然后在accept()函数里面这样传递参数(注意传递地址) ac

c语言-C语言段错误 非常急 大神们 求助

问题描述 C语言段错误 非常急 大神们 求助 我再写一个c的字典项目 dict.txt文件里有 20多万行单词 运行有段错误 把dict里的 单词减少到1万 就可以了 这是为什么 我用的链表 大神们帮我解决啊 解决方案 编写程序,重要的是做好错误处理,判断出错时是哪句出错了. 1.程序动态申请的内存比较大,应该检查一下是否申请失败,就是malloc之后,判断q是否为NULL 2.数据量这么大,也许有的数据格式不对,应该对fgets是否失败也做个检查. if(ferror(fp)) { print

c语言-帮忙看下C语言打印错误

问题描述 帮忙看下C语言打印错误 #include int main(void) { int n,i,j,t; scanf("%d",&n); for(i=1;i<=4;i++){ for (j=1;j<=n-i;j++) printf(" "); for(t=1;t<=2i-1;t++) printf("*"); printf("n");} for(i=3;i>=1;i--){ for (j=

C语言常见错误集合

C语言的最大特点是:功能强.使用方便灵活.C编译的程序对语法检查并不象其它高级语言那么严格,这就给编程人员留下"灵活的余地",但还是由于这个灵活给程序的调试带来了许多不便,尤其对初学C语言的人来说,经常会出一些连自己都不知道错在哪里的错误.看着有错的程序,不知该如何改起,本人通过对C的学习,积累了一些C编程时常犯的错误,写给各位学员以供参考. 1.书写标识符时,忽略了大小写字母的区别. main() { int a=5; printf("%d",A); } 编译程序

r语言-(错误在哪里)R语言如何从文件中读取数据

问题描述 (错误在哪里)R语言如何从文件中读取数据 file.choose() [1] "C:UsersHPDesktop新建文件夹data.txt" data1<-read.table("data.txt",header=T) 错误于make.names(col.names, unique = TRUE) : ''多字节字符串有错 data1<-read.table("data.txt",header=T,quote = FALSE

基于C语言中段错误的问题详解_C 语言

当我在linux下写c语言的时候经常会遇到段错误.所以就来细究一下. 段错误或段违规(segmentation violation)查看Expert C Programming(Peter Van Der Linden) Pg.156解释到段错误是由于内存管理单元(MMU)的异常所致,而该异常则通常是由于解除引用一个未初始化或非法的指针引起. 就是指针正在引用一个并不位于你的地址空间中的地址.书中的例子 复制代码 代码如下: int *p = 0;  *p = 17;  这里显然 地址0 并不是

语言-没有错误,没有警告,输入数值后,结果不对,求解 帮帮忙,

问题描述 没有错误,没有警告,输入数值后,结果不对,求解 帮帮忙, /*用递归法将一个整数n转换为相应的字符串,例如,输入483,输出的是字符串"483",n的位数不确定,可以是任意的正 整数.(说明详见实验指导书P264第(2)题)请在下面空白处,填写相应的程序段,并将程序保存于文件ex5_2.c中.*/ #include #include char c[1000]; void int_to_string(int n,int w) {int i=2,j=w-1,m=10; c[w+1