在Go中创建自定义错误

在向用户传达更复杂的错误信息时,或在调试时向未来的自己传达错误信息时,有时这两种机制不足以充分捕获和报告发生的情况。为了传达这个更复杂的错误信息,我们可以实现标准库接口类型`error`,以获得更多功能。

介绍

Go提供了两种在标准库中创建错误的方法, errors.Newfmt.Errorf 在向用户传达更复杂的错误信息时,或在调试时向未来的自己传达错误信息时,有时这两种机制不足以充分捕获和报告发生的情况。 为了传达这种更复杂的错误信息并获得更多功能,我们可以实现标准的库接口类型, error

其语法如下:

type error interface {
  Error() string
}

builtin包将error定义为具有单个Error()方法的接口,该方法将错误消息作为字符串返回。 通过实现此方法,我们可以将我们定义的任何类型转换为我们自己的错误。

让我们尝试运行以下示例来查看error接口的实现:

package main

import (
    "fmt"
    "os"
)

type MyError struct{}

func (m *MyError) Error() string {
    return "boom"
}

func sayHello() (string, error) {
    return "", &MyError{}
}

func main() {
    s, err := sayHello()
    if err != nil {
        fmt.Println("unexpected error: err:", err)
        os.Exit(1)
    }
    fmt.Println("The string:", s)
}

我们将看到以下输出:

unexpected error: err: boom
exit status 1

这里我们创建了一个新的空结构类型MyError ,并在其上定义了Error()方法。 Error()方法返回字符串"boom"

main() ,我们调用函数sayHello ,它返回一个空字符串和一个新的MyError实例。 由于sayHello将始终返回错误,因此main() if语句的主体内的fmt.Println调用将始终执行。 然后我们使用fmt.Println打印短前缀字符串"unexpected error:"以及在err变量中保存的MyError实例。

请注意,我们不必直接调用Error() ,因为fmt包能够自动检测到这是一个error的实现。 透明地调用Error()来获取字符串"boom"并将其与前缀字符串"unexpected error: err:"

收集自定义错误中的详细信息

有时,自定义错误是捕获详细错误信息的最简洁方法。 例如,假设我们想捕获HTTP请求产生的错误的状态代码; 运行以下程序以查看允许我们干净地捕获该信息的error实现:

package main

import (
    "errors"
    "fmt"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("success!")
}

我们将看到以下输出:

status 503: err unavailable
exit status 1

在此示例中,我们创建了一个新的RequestError实例,并使用标准库中的errors.New函数提供状态代码和错误。 然后,我们使用fmt.Println打印它,如前面的示例所示。

RequestErrorError()方法中,我们使用fmt.Sprintf函数使用创建错误时提供的信息构造字符串。

键入断言和自定义错误

error接口只公开一种方法,但我们可能需要访问其他error实现方法才能正确处理错误。 例如,我们可能有几个临时的自定义error实现,可以重试 - 由Temporary()方法表示。

接口提供了由类型提供的更广泛的方法集的狭窄视图,因此我们必须使用类型断言来更改视图正在显示的方法,或者完全删除它。

以下示例将前面显示的RequestError扩展为具有Temporary()方法,该方法将指示调用方是否应重试请求:

package main

import (
    "errors"
    "fmt"
    "net/http"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return r.Err.Error()
}

func (r *RequestError) Temporary() bool {
    return r.StatusCode == http.StatusServiceUnavailable // 503
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        re, ok := err.(*RequestError)
        if ok {
            if re.Temporary() {
                fmt.Println("This request can be tried again")
            } else {
                fmt.Println("This request cannot be tried again")
            }
        }
        os.Exit(1)
    }

    fmt.Println("success!")
}

我们将看到以下输出:

unavailable
This request can be tried again
exit status 1

main() ,我们调用doRequest() ,它向我们返回一个error接口。 我们首先打印Error()方法返回的错误消息。 接下来,我们尝试使用类型断言re, ok := err.(*RequestError)RequestError公开所有方法。 如果类型断言成功,我们然后使用Temporary()方法来查看此错误是否是临时错误。 由于doRequest()设置的StatusCode503 ,与http.StatusServiceUnavailable匹配,因此返回true并导致打印"This request can be tried again" 实际上,我们会提出另一个请求而不是打印消息。

包装错误

通常,会从程序外部的某些内容生成错误,例如:数据库,网络连接等。这些错误提供的错误消息无法帮助任何人找到错误的来源。 在错误消息开头包含额外信息的错误将为成功调试提供一些必要的上下文。

以下示例演示了如何将一些上下文信息附加到从其他函数返回的其他神秘error

package main

import (
    "errors"
    "fmt"
)

type WrappedError struct {
    Context string
    Err     error
}

func (w *WrappedError) Error() string {
    return fmt.Sprintf("%s: %v", w.Context, w.Err)
}

func Wrap(err error, info string) *WrappedError {
    return &WrappedError{
        Context: info,
        Err:     err,
    }
}

func main() {
    err := errors.New("boom!")
    err = Wrap(err, "main")

    fmt.Println(err)
}

我们将看到以下输出:

main: boom!

WrappedError是一个包含两个字段的结构:一个上下文消息作为string ,以及此WrappedError提供有关的更多信息的error 当调用Error()方法时,我们再次使用fmt.Sprintf打印上下文消息,然后发生errorfmt.Sprintf知道隐式调用Error()方法)。

main() ,我们使用errors.New创建一个错误,然后我们使用我们定义的Wrap函数包装该错误。 这允许我们指示此error是在"main"生成的。 此外,由于我们的WrappedError也是一个error ,我们可以包装其他WrappedError - 这将允许我们看到一个链来帮助我们追踪错误的来源。 在标准库的帮助下,我们甚至可以在错误中嵌入完整的跟踪。

结论

由于error接口只是一种方法,我们已经看到我们在为不同情况提供不同类型的错误方面具有很大的灵活性。 这可以涵盖从将多条信息作为错误的一部分传递到实现指数退避的所有内容 虽然Go中的错误处理机制可能表面上看似简单,但我们可以使用这些自定义错误来处理相当丰富的处理,以处理常见和不常见的情况。

Go有另一种机制来传达意外行为,恐慌。 在我们错误处理系列的下一篇文章中,我们将研究恐慌 - 它们是什么以及如何处理它们。