如何比较Go语言中的错误?

84

我有一个错误值,将它打印到控制台上会显示Token is expired

我该如何将其与特定的错误值进行比较?我尝试过这样,但不起作用:

if err == errors.New("Token is expired") {
      log.Printf("Unauthorised: %s\n", err)
}

6
我建议避免使用常规方法。请参考 Dαve Cheney 的关于错误处理的演讲 https://www.youtube.com/watch?v=lsBF58Q-DnY。我一定会回答你的问题。 - s7anley
11个回答

101
声明一个错误,并将其与“==”进行比较(如err == myPkg.ErrTokenExpired)不再是Go 1.13(2019年第三季度)的最佳实践方法。 发行说明中提到:
Go 1.13 包含支持错误包装的功能,如错误值提案中首次提出并在相关问题上讨论。一个错误e可以通过提供返回wUnwrap方法来包装另一个错误w。程序可使用ew,允许ew提供额外的上下文或重新解释它,同时仍然允许程序基于w做出决策。为了支持包装,fmt.Errorf现在有一个%w动词用于创建包装错误,并在errors包中增加了三个新函数(errors.Unwrap, errors.Iserrors.As),简化了解包装错误和检查包装错误。

因此,错误值FAQ解释道:

You need to be prepared that errors you get may be wrapped.

If you currently compare errors using ==, use errors.Is instead.
Example:

if err == io.ErrUnexpectedEOF

becomes

if errors.Is(err, io.ErrUnexpectedEOF)
  • Checks of the form if err != nil need not be changed.
  • Comparisons to io.EOF need not be changed, because io.EOF should never be wrapped.

If you check for an error type using a type assertion or type switch, use errors.As instead. Example:

if e, ok := err.(*os.PathError); ok

becomes

var e *os.PathError
if errors.As(err, &e)

Also use this pattern to check whether an error implements an interface. (This is one of those rare cases when a pointer to an interface is appropriate.)

Rewrite a type switch as a sequence of if-elses.


7
你真的值得拥有100万声望! - Yasser Sinjab

53

这个答案适用于Go 1.12及之前的版本。

在库中定义一个错误值

package fruits

var NoMorePumpkins = errors.New("No more pumpkins")

在代码的任何地方都不要使用errors.New创建错误,而应在发生错误时返回预定义的值,然后可以执行以下操作:

package shop

if err == fruits.NoMorePumpkins {
     ...
}

参考 io 包的错误。

通过添加方法来隐藏检查实现并使客户端代码对 fruits 包中的更改更加免疫,以改进此功能。

package fruits

func IsNoMorePumpkins(err error) bool {
    return err == NoMorePumpkins
} 

请参考os包中的错误信息。


这样做的一个缺点是,另一个包有可能会覆盖导出的 NoMorePumpkins 变量的值。如果能用 const 来实现就好了。 - captncraig
1
@captncraig 那你可以使用一个 getter 函数。 - wingerse
@WingerSendon 很好的观点。那可能是我最喜欢的解决方案了。 - captncraig
但是你为什么要覆盖别人的包错误呢? - math2001
4
实际问题不是有人会覆盖 fruits.NoMorePumpkins,而是他们可能包装它,导致 == 失败。 - cbednarski
1
@cbednarski 请查看Go 1.13的新错误内置功能。现在有一种新的方法可以制作分层错误,以便您可以检查它是否被包装。 - d1str0

34

尝试

err.Error() == "Token is expired"

或者通过实现错误接口来创建自己的错误。


39
这是一个糟糕的建议。如果错误文本发生变化怎么办?你应该将其与一个错误“变量”进行比较。 - user187676
1
错误文本通常是特定于实现的,不属于软件包的契约,因此这是不可取的。虽然如果在内部进行(一旦确保错误字符串不会更改),这样做是安全的,但为了保持一致性和可读性,我建议使用自定义错误类型。 - David Callanan
12
我不知道为什么我的评论一直被删除!这个答案不是建议,所以不能被称为“糟糕的建议”。问题本身涉及字符串字面值的比较。我试图回答问题,而不是提供最佳实践建议。 - Sridhar

20

习惯上,包会导出它们使用的错误变量,以便他人可以与它们进行比较。

例如,如果来自名为 myPkg 的包的一个错误被定义为:

var ErrTokenExpired error = errors.New("Token is expired")

您可以直接将错误进行比较:

if err == myPkg.ErrTokenExpired {
    log.Printf("Unauthorised: %s\n", err)
}
如果错误来自第三方包并且该包没有使用导出的错误变量,那么您可以简单地从 err.Error() 获取字符串并进行比较,但是要小心使用此方法,因为更改错误字符串可能不会在主要版本中发布,并会破坏您的业务逻辑。

2
比较字符串好很多。 - luben
1
你不应该测试第三方错误,因为这些错误是实现细节。例如,如果你有一个数据库包,本地实现使用了一个映射,如果某个项不存在,可能会出现键错误(在Go的情况下,ok将为false)。如果它改用SQL后端,则可能会出现行未找到错误。在我看来,包永远不应直接传播错误。要么a)将它们映射到更合适的自定义错误类型(例如前面示例中的未找到项目错误),要么b)用自定义“实现错误”类型包装它,或者c)引发panic。 - David Callanan

17
错误类型是一个接口类型。错误变量代表可以描述自身为字符串的任何值。以下是该接口的声明:
type error interface {
    Error() string
}

最常用的错误实现是errors包中未导出的errorString类型:

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

请查看这个代码的输出结果 (Go Playground):
package main

import (
    "errors"
    "fmt"
    "io"
)

func main() {
    err1 := fmt.Errorf("Error")
    err2 := errors.New("Error")
    err3 := io.EOF

    fmt.Println(err1)         //Error
    fmt.Printf("%#v\n", err1) // &errors.errorString{s:"Error"}
    fmt.Printf("%#v\n", err2) // &errors.errorString{s:"Error"}
    fmt.Printf("%#v\n", err3) // &errors.errorString{s:"EOF"}
}

输出:

Error
&errors.errorString{s:"Error"}
&errors.errorString{s:"Error"}
&errors.errorString{s:"EOF"}

另请参见:比较运算符

比较运算符用于比较两个操作数并生成一个未命名的布尔值。在任何比较中,第一个操作数必须可以赋值给第二个操作数的类型,反之亦然。

等号运算符 ==!= 适用于可比较的操作数。

指针值是可比较的。如果两个指针值指向同一变量或都具有值为 nil,则它们相等。指向不同零大小变量的指针可能相等,也可能不相等。

接口值是可比较的。如果两个接口值具有相同的动态类型和相等的动态值,或者都具有值为 nil,则它们相等。

对于非接口类型 X 的值 x 和接口类型 T 的值 t,当 X 的值是可比较的并且 X 实现了 T 时,它们是可比较的。如果 t 的动态类型与 X 相同,并且 t 的动态值等于 x,则它们相等。

如果所有字段都是可比较的,则结构值是可比较的。如果它们相应的非空白字段相等,则两个结构值相等。


所以:

1- 您可以使用 Error(),像这个可工作的代码一样 (Go Playground):

package main

import (
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("Token is expired")
    err2 := errors.New("Token is expired")
    if err1.Error() == err2.Error() {
        fmt.Println(err1.Error() == err2.Error()) // true
    }
}

输出:

true

2- 你也可以将其与nil进行比较,像这个可行的代码一样(Go Playground):

package main

import (
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("Token is expired")
    err2 := errors.New("Token is expired")
    if err1 != nil {
        fmt.Println(err1 == err2) // false
    }
}

输出:

false

3- 此外,您可以将其与完全相同的错误进行比较,就像这个工作代码一样
(Go Playground):

package main

import (
    "fmt"
    "io"
)

func main() {
    err1 := io.EOF
    if err1 == io.EOF {
        fmt.Println("err1 is : ", err1)
    }
}

输出:

err1 is :  EOF

参考文献:https://blog.golang.org/error-handling-and-go

在IT技术中,错误处理是一项重要的任务。Go语言提供了几种方法来处理错误,其中最常见的是返回错误值。如果函数发生错误,它将返回一个非空的错误值,否则它将返回nil。为了使代码更加简洁,可以使用defer和panic机制来处理错误。此外,还可以使用recover函数来恢复从panic状态中退出的程序。这些方法都有助于简化代码并提高可读性。

14

不鼓励通过字符串比较错误。相反,应该按值比较错误。

package main

import "errors"

var NotFound = errors.New("not found")

func main() {
    if err := doSomething(); errors.Is(err, NotFound) {
        println(err)
    }
}

func doSomething() error {
    return NotFound
}

如果你是一个库的作者并且想要导出错误以便用户可以根据不同类型的错误采取不同的行动,那么这将非常有用。标准库也实现了这一点。

但是这种方法的问题在于,由于Go不支持不可变值,导出的值可能会被任何人更改。不过,你仍然可以使用字符串作为错误并将其设置为const

package main

type CustomError string

func (ce CustomError) Error() string {
    return string(ce)
}

const NotFound CustomError = "not found"

func main() {
    if err := doSomething(); errors.Is(err, NotFound) {
        println(err)
    }
}

func doSomething() error {
    return NotFound
}

这种方法更冗长一些,但更安全。


5

你应该首先考虑按值比较错误,就像其他解决方案中所描述的那样:

if errors.Is(err1, err2) {
  // do sth
}

然而,在某些情况下,从函数返回的错误信息可能有点复杂,例如,错误被多次包装,并且在每个函数调用中添加了上下文,例如 fmt.Errorf("一些上下文:%w", err),而您可能只想比较两个错误的错误信息。在这种情况下,您可以执行以下操作:

// SameErrorMessage checks whether two errors have the same messages.
func SameErrorMessage(err, target error) bool {
    if target == nil || err == nil {
        return err == target
    }
    return err.Error() == target.Error()
}

func main() {
  ...
  if SameErrorMessage(err1, err2) {
     // do sth
  }

}

请提供更多的需要翻译的内容。
if err1.Error() == err2.Error() {
  // do sth
}

如果 err1err2nil,那么您可能会遇到空指针解引用的运行时错误。

3
为了补充@wst的回答,在某些情况下,errors.Is(err, NotFound)方法可能无法正常工作,原因我也在努力寻找中。如果有人知道,请在评论中告诉我。

enter image description here

但我发现以下方法更好,适用于我的情况:
if NotFound.Is(err) {
    // do something
}

在这里,var NotFound = errors.New("not found") 是一个公共的导出错误。

在我的情况下,解决方案是

if models.GetUnAuthenticatedError().Is(err) {
    // Do something
}

0
errors.Is 只适用于常量错误。如果错误是动态创建的,例如包含上下文信息,新的错误实例无法与之进行比较。在这种情况下,必须使用 error.As

0

我想发布一个案例,其中errors.Is可以很好地处理具有非可比值的自定义错误。

type CustomError struct {
    Meta    map[string]interface{}
    Message string
}

func (c CustomError) Error() string {
    return c.Message
}

var (
    ErrorA = CustomError{Message: "msg", Meta: map[string]interface{}{"key": "value"}}
)

func DoSomething() error {
    return ErrorA
}

func main() {
    err := DoSomething()
    if errors.Is(err, ErrorA) {
        fmt.Println("error is errorA")
    } else {
        fmt.Println("error is NOT errorA")
    }
}

输出

error is NOT errorA

游乐场


根本原因

原因是errors.Is检查target是否可比较。

func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()

Go语言中的comparable类型有:

布尔型、数字型、字符串型、指针型、通道型、可比较类型的数组、所有字段都是可比较类型的结构体

由于CustomErrorMeta map[string]interface{}不是可比较的,所以errors.Is检查失败了。


解决方法

  • 一个解决方法是将ErrorA = &CustomError{Message: "msg", Meta: map[string]interface{}{"key": "value"}}声明为指针。
  • 您可以实现CustomError.Is(target error)来比较这个自定义类型。参考@kaizenCoder的评论。
func (c CustomError) Is(err error) bool {
    cErr, ok := err.(CustomError)
    if !ok {
        return false
    }

    if c.Message != cErr.Message || fmt.Sprint(c.Meta) != fmt.Sprint(c.Meta) {
        return false
    }

    return true
}

    err := DoSomething()
    if ErrorA.Is(err) {
        fmt.Println("error is errorA")
    } else {
        fmt.Println("error is NOT errorA")
    }

游乐场


1
你可以实现一个 CustomError.Is(target error) 方法来比较这个自定义类型。 - kaizenCoder
1
你可以实现一个 CustomError.Is(target error) 方法来对这个自定义类型进行比较。 - undefined
@kaizenCoder,感谢您的评论,我的回答已经更新。 - zangw

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