在 Go 中创建自定义错误

简介

Go提供了两种在标准库中创建错误的方法errors.Newfmt.Errorf.当在调试时向您的用户或您将来的自己传达更复杂的错误信息时,有时这两种机制不足以充分捕获和报告所发生的事情。为了传递这种更复杂的错误信息并获得更多功能,我们可以实现标准库接口类型[error](https://golang.org/pkg/builtin/ error).

其语法如下所示:

1type error interface {
2  Error() string
3}

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

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

 1package main
 2
 3import (
 4    "fmt"
 5    "os"
 6)
 7
 8type MyError struct{}
 9
10func (m *MyError) Error() string {
11    return "boom"
12}
13
14func sayHello() (string, error) {
15    return "", &MyError{}
16}
17
18func main() {
19    s, err := sayHello()
20    if err != nil {
21    	fmt.Println("unexpected error: err:", err)
22    	os.Exit(1)
23    }
24    fmt.Println("The string:", s)
25}

我们将看到以下输出:

1[secondary_label Output]
2unexpected error: err: boom
3exit status 1

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

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

请注意,我们不必直接调用Error(),因为fmt包能够自动检测到这是error的实现。它调用Error()transparently来获取字符串boom,并将其与前缀字符串意外错误:Err:连接在一起。

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

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

 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6    "os"
 7)
 8
 9type RequestError struct {
10    StatusCode int
11
12    Err error
13}
14
15func (r *RequestError) Error() string {
16    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
17}
18
19func doRequest() error {
20    return &RequestError{
21    	StatusCode: 503,
22    	Err:        errors.New("unavailable"),
23    }
24}
25
26func main() {
27    err := doRequest()
28    if err != nil {
29    	fmt.Println(err)
30    	os.Exit(1)
31    }
32    fmt.Println("success!")
33}

我们将看到以下输出:

1[secondary_label Output]
2status 503: err unavailable
3exit status 1

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

RequestErrorError()方法中,我们使用fmt.Sprint tf函数来构造一个字符串,该字符串使用在错误产生时提供的信息。

类型断言和自定义错误

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

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

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

 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6    "net/http"
 7    "os"
 8)
 9
10type RequestError struct {
11    StatusCode int
12
13    Err error
14}
15
16func (r *RequestError) Error() string {
17    return r.Err.Error()
18}
19
20func (r *RequestError) Temporary() bool {
21    return r.StatusCode == http.StatusServiceUnavailable // 503
22}
23
24func doRequest() error {
25    return &RequestError{
26    	StatusCode: 503,
27    	Err:        errors.New("unavailable"),
28    }
29}
30
31func main() {
32    err := doRequest()
33    if err != nil {
34    	fmt.Println(err)
35    	re, ok := err.(*RequestError)
36    	if ok {
37    		if re.Temporary() {
38    			fmt.Println("This request can be tried again")
39    		} else {
40    			fmt.Println("This request cannot be tried again")
41    		}
42    	}
43    	os.Exit(1)
44    }
45
46    fmt.Println("success!")
47}

我们将看到以下输出:

1[secondary_label Output]
2unavailable
3This request can be tried again
4exit status 1

main()中,我们调用doRequest(),它向我们返回一个error接口。我们首先打印Error()方法返回的错误消息。接下来,我们尝试通过类型断言re,ok:=err.(* RequestError)公开来自RequestError的所有方法。如果类型断言成功,则使用Temporary()方法来确定此错误是否是临时性错误。由于doRequest()设置的StatusCode503,匹配HTTP.StatusServiceUnavailable,因此返回true,并导致打印‘可以重试此请求’。实际上,我们会发出另一个请求,而不是打印消息。

包装错误

通常,错误是由程序之外的东西生成的,例如:数据库、网络连接等。这些错误提供的错误消息不能帮助任何人找到错误的根源。在错误消息的开头用额外信息包装错误将为成功调试提供一些所需的上下文。

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

 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6)
 7
 8type WrappedError struct {
 9    Context string
10    Err error
11}
12
13func (w *WrappedError) Error() string {
14    return fmt.Sprintf("%s: %v", w.Context, w.Err)
15}
16
17func Wrap(err error, info string) *WrappedError {
18    return &WrappedError{
19    	Context: info,
20    	Err:     err,
21    }
22}
23
24func main() {
25    err := errors.New("boom!")
26    err = Wrap(err, "main")
27
28    fmt.Println(err)
29}

我们将看到以下输出:

1[secondary_label Output]
2main: boom!

WrapedError是一个结构,有两个字段:一个是作为字符串的上下文消息,另一个是error,这个WrapedError提供了更多的信息。当调用Error()方法时,我们再次使用fmt.Sprint tf打印上下文消息,然后error(fmt.Sprint tf也知道隐式调用Error()方法)。

main()中,我们使用errors.New创建一个错误,然后使用我们定义的Wrap函数包装该错误。这允许我们指出这个错误是在main中生成的。此外,因为我们的`WrapedError‘也是一个’Error‘,所以我们可以包装其他的’WrapedError‘--这将允许我们看到一个链来帮助我们追踪错误的来源。在标准库的帮助下,我们甚至可以在错误中嵌入完整的堆栈跟踪。

结论

由于)的所有内容。虽然Go语言中的错误处理机制表面上看起来过于简单,但我们可以使用这些自定义错误来处理常见和不常见的情况。

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

Published At
Categories with 技术
Tagged with
comments powered by Disqus