在 Golang 中,我们如何组合多个错误字符串?

68

我是golang的新手,我的应用程序需要在循环中返回多个错误,稍后需要将它们组合并作为单个错误字符串返回。我无法使用字符串函数来组合错误消息。在返回之前,有哪些方法可以用来将这些错误组合成一个单一的错误?

package main

import (
   "fmt"
   "strings"
)

func Servreturn() (err error) {

   err1 = fmt.Errorf("Something else occurred")
   err2 = fmt.Errorf("Something else occurred again")

   // concatenate both errors

   return err3

}

不足的信息。客户端是做什么的?你的服务器是做什么的?一个样本输出。 - ZAky
未来版本(Go 1.2x,预计在2023年)应该提供一种返回错误切片/树的方法。 - VonC
downvote:连接字符串消息并不那么有趣,我正在寻找一种保留错误对象的方法,而这个问题似乎与堆叠或连接错误无关。 - Ярослав Рахматуллин
Go 1.20增加了多错误包装功能。请参见此答案 - Inanc Gumus
11个回答

100

Go 1.20更新:

Go版本1.20开始,您可以使用新的errors.Joinfunction(忽略nil错误)加入错误

err = errors.Join(err, nil, err2, err3)

错误演示场。加入

此外,fmt.Errorf函数现在支持使用多个%w格式占位符包裹多个错误:

err := fmt.Errorf("%w; %w; %w", err, err2, err3)

fmt.Errorf的游乐场示例


Go 1.13更新:

Go版本1.13开始,该语言的错误包现在直接支持错误包装

您可以使用fmt.Errorf中的%w动词来包装错误:

err := errors.New("Original error")
err = fmt.Errorf("%w; Second error", err)

使用Unwrap来移除最后添加的错误,并返回剩余的内容:previousErrors := errors.Unwrap(err) errors.Unwrap的示例 另外两个函数,errors.Iserrors.As提供了检查和检索特定类型错误的方法。 errors.As和errors.Is的示例
Dave Cheney的优秀的errors包(https://github.com/pkg/errors)包括一个Wrap函数,用于此目的:
package main

import "fmt"
import "github.com/pkg/errors"

func main() {
        err := errors.New("error")
        err = errors.Wrap(err, "open failed")
        err = errors.Wrap(err, "read config failed")

        fmt.Println(err) // "read config failed: open failed: error"
}

这也允许额外的功能,例如解包错误的原因:

package main

import "fmt"
import "github.com/pkg/errors"

func main() {
    err := errors.New("original error")
    err = errors.Wrap(err, "now this")

    fmt.Println(errors.Cause(err)) // "original error"
}

同时,当指定fmt.Printf("%+v\n", err)时,还可以选择输出堆栈跟踪。

您可以在他的博客此处此处找到有关该软件包的更多信息。


56
包装错误不是为了处理多个错误,而是为了将一个错误与更多上下文信息一起包装。 - dolmen
@dolmen,你能提供一份关于这个说法的引用吗? - Paulo Neves
@PauloNeves 到目前为止,有17人批准了我的评论。 - dolmen
@dolmen 这是什么意思? - Paulo Neves
2
@Paulo:@dolmen 断言错误包装在其他语言中像 InnerException 一样使用最直观。一个包装的错误不是(同级)错误列表,而是(父子)错误链。没有什么能阻止你像列表一样使用它,但在我看来这是违反直觉的。 - Devin Burke
1
Go 1.20原生支持错误处理Join。有关更多详细信息,请参见此答案: https://dev59.com/oVwX5IYBdhLWcg3wtBbG#74645028 - Inanc Gumus

29

字符串函数不能对错误进行操作,因为错误实际上是一个实现了函数Error() string的接口。

你可以在err1.Error()和err2.Error()上使用字符串函数,但不能在"err1"引用本身上使用。

有些错误是结构体,例如从数据库驱动程序中获取的错误。

因此,在错误上使用字符串函数没有自然的方法,因为它们可能实际上并不是字符串。

至于如何组合两个错误:

很简单,只需再次使用fmt.Errorf即可。

fmt.Errorf("Combined error: %v %v", err1, err2)

或者:

errors.New(err1.Error() + err2.Error())

除了使用 fmt.Errorf(),我为什么不能使用字符串函数? - Greg Petr
当然可以。这取决于你所说的“合并”是什么意思。字符串连接使用+运算符完成。或者您可以将它们单独存储在一个切片中,使用append()函数向其中添加值。这个问题建议您通过Go教程自学,并获取一本相关书籍。 - kostix
1
@GregPetr 另一种可能更加简洁的方法是创建自己的 CombinedError 结构体,将错误列表作为一个字段传入,并实现 Error() string 方法,然后根据需要进行字符串拼接或其他操作。 - Will C

28
你可以使用 strings.Join()append() 函数来实现这个切片。 示例:golang playgorund
package main

import (
    "fmt"
    "strings"
    "syscall"
)

func main() {

    // create a slice for the errors
    var errstrings []string 

    // first error
    err1 := fmt.Errorf("First error:server error")
    errstrings = append(errstrings, err1.Error())

    // do something 
    err2 := fmt.Errorf("Second error:%s", syscall.ENOPKG.Error())
    errstrings = append(errstrings, err2.Error())

    // do something else
    err3 := fmt.Errorf("Third error:%s", syscall.ENOTCONN.Error())
    errstrings = append(errstrings, err3.Error())

    // combine and print all the error
    fmt.Println(fmt.Errorf(strings.Join(errstrings, "\n")))


}

这将输出一个字符串,您可以将其发送回客户端。

First error:server1 
Second error:Package not installed 
Third error:Socket is not connected

希望这能有所帮助!


1
现在您可以使用errors.Join代替:https://dev59.com/oVwX5IYBdhLWcg3wtBbG#74645028 - Inanc Gumus

13
从Go 1.20开始,我们将能够使用errors.Join来包装多个错误。

有关更多详细信息,请参见this proposal

第一选项

您可以打印这些错误;这样做时它们将被换行符分隔。

var (
    ErrIncorrectUsername = errors.New("incorrect username")
    ErrIncorrectPassword = errors.New("incorrect password")
)

func main() {
    err := validate("ruster", "4321")
    // You can print multi-line errors
    // Each will be separated by a newline character (\n).
    if err != nil {
        fmt.Println(err)
        // incorrect username
        // incorrect password
    }
}

func validate(username, password string) error {
    var errs []error

    // errors.Join the errors into a single error
    if username != "gopher" {
        errs = append(errs, ErrIncorrectUsername)
    }
    if password != "1234" {
        errs = append(errs, ErrIncorrectPassword)
    }

    // Join returns a single `error`.
    // Underlying, the error contains all the errors we add.
    return errors.Join(errs...)
}

第二个选项

errors.Join 返回一个包含您添加的每个错误的 error。因此,您可以使用 errors.Iserrors.As 检查单个错误以获得更细粒度的控制。

// ...
func main() {
    err := validate("ruster", "4321")
    // You can detect each one:
    if errors.Is(err, ErrIncorrectUsername) {
        // handle the error here
    }
    // Or detect the other one:
    if errors.Is(err, ErrIncorrectPassword) {
        // handle the error here
    }
}

func validate(username, password string) error {
    // ...
}

注意:这个简单的validate示例仅用于传达思想。不要像链式一样考虑错误,而是将错误看作树形结构。与其他Join组合使用时,Join可以实现这一点。

在 Go Playground 上同时运行


12

针对@WillC在评论中提到的内容,可以定义你自己的error类型,因为error是一个接口类型。任何实现了 Error() string 函数的类型都实现了error接口。因此,您可以创建一个CollectionError,它汇总错误并返回连接的错误字符串。

type ErrorCollector []error

func (c *ErrorCollector) Collect(e error) { *c = append(*c, e) }

func (c *ErrorCollector) Error() (err string) {
    err = "Collected errors:\n"
    for i, e := range *c {
        err += fmt.Sprintf("\tError %d: %s\n", i, e.Error())
    }

    return err
}

这提供了一个集合函数,它将给定的error附加到一个切片中。在调用Error() string时,它会遍历切片并创建一个连接的错误字符串。

func main() {
    collector := new(ErrorCollector)

    for i := 0; i < 10; i++ {
         collector.Collect(errors.New(fmt.Sprintf("%d Error", i)))
    }

    fmt.Println(collector)
}

有一篇很棒的 golang.org博客文章 对错误进行了更详细的介绍。一个完整的示例可以在The Go Playground上找到。


谢谢,你的示例非常有用。我会尝试一下并回来。 - Greg Petr
@BenCampbell 这里方法接收器为什么要是一个指针呢? - Rakesh
谢谢这个提示。以防万一添加了一个空错误,Error()函数应该在收集错误时进行空检查。如果只是通过索引访问而不是索引+值,则需要在索引访问之前解引用转换错误数组。这里有一个示例:https://play.golang.org/p/h9F5WhcQ5Bs - David
@Rakesh,应该把 func (c *ErrorCollector) Error() (err string) 修改为 func (c ErrorCollector) Error() (err string) - dolmen
你不一定需要这样做,但是如果有些方法是指针接收器,而有些方法是值接收器,则会违反惯例。Collect 方法需要是指针接收器。 - jdizzle

11

Uber有一个适用于此用例的multierr包:

return multierr.Combine(err1, err2)

从Go 1.20开始,它被本地支持:https://dev59.com/oVwX5IYBdhLWcg3wtBbG#74645028。 - Inanc Gumus

7

4
它附带MPL许可证,要求您披露源代码。 - Samir Kape

3

以下内容对我来说似乎很有效(以空格分隔的错误):

  1. 将所有错误放入一个错误切片/列表/数组中,例如:var errors []error
  2. fmt.Sprintf("%s", errors)
    var errors []error
    errors = append(errors, fmt.Errorf("error 1"))
    errors = append(errors, fmt.Errorf("error 2"))
    errors = append(errors, fmt.Errorf("and yet another error"))
    s := fmt.Sprintf("%s", errors)
    fmt.Println(s)

1
有时我需要检测链中是否存在错误。提供的标准方式非常方便,使用 https://pkg.go.dev/errors 可以实现:
someErr:=errors.New("my error")
fmt.Errorf("can't process request: %w",someErr) 
...
err:=f()
if errors.Is(err,someErr){...}

但是它只能应用于在链中检测到最后一个错误的情况。您不能将someErr1和someErr2包装起来,然后从这两个检查中获得真值:errors.Is(err,someErr1)和errors.Is(err,someErr2)。
我用以下类型解决了这个问题:
func NewJoinedErrors(err1 error, err2 error) JoinedErrors {
    return JoinedErrors{err1: err1, err2: err2}
}

type JoinedErrors struct {
    err1 error
    err2 error
}

func (e JoinedErrors) Error() string {
    return fmt.Sprintf("%s: %s", e.err1, e.err2)
}

func (e JoinedErrors) Unwrap() error {
    return e.err2
}

func (e JoinedErrors) Is(target error) bool {
    return errors.Is(e.err1, target)
}

它利用了这个事实:
一个错误如果等于目标,或者实现了一个方法 Is(error) bool 使得 Is(target) 返回 true,那么该错误被认为与目标匹配。
因此,你可以结合两个错误,并在两个检查中获得积极的结果:
someErr1:=errors.New("my error 1")
someErr2:=errors.New("my error 2")
err:=NewJoinedErrors(someErr1, someErr2)
// this will be true because 
// (e JoinedErrors) Is(target error) 
// will return true 
if errors.Is(err, someErr1){...}
// this will be true because 
// (e JoinedErrors) Unwrap() error 
// will return err2 
if errors.Is(err, someErr2){...}

你可以在这里查看: https://play.golang.org/p/W7NGyfvr0v_N


0
func condenseErrors(errs []error) error {
    switch len(errs) {
    case 0:
        return nil
    case 1:
        return errs[0]
    }
    err := errs[0]
    for _, e := range errs[1:] {
        err = errors.Wrap(err, e.Error())
    }
    return err
}

请注意,如果第一个错误为nil,例如[nil,error1,error2,error3],由于Wrap实现,您的代码将返回nil。func Wrap(err error, message string) error { if err == nil { return nil } err = &withMessage{ cause: err, msg: message, } return &withStack{ err, callers(), } } - igorushi
好的观点。这假设传递给函数的 errs 不是 nil。 - Sebastian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接