在Go中创建自定义错误
介绍
Go 在标准库中提供了两种创建错误的方法,errors.New 和 fmt.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) }
我们将看到以下输出:
Outputunexpected 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()
[1] 透明地获取字符串 "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!") }
我们将看到以下输出:
Outputstatus 503: err unavailable exit status 1
在本例中,我们创建了一个 RequestError
的新实例,并使用标准库中的 errors.New
函数提供状态代码和错误。 然后我们像前面的例子一样使用 fmt.Println
打印它。
在 RequestError
的 Error()
方法中,我们使用 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!") }
我们将看到以下输出:
Outputunavailable This request can be tried again exit status 1
在 main()
中,我们调用 doRequest()
,它返回一个 error
接口给我们。 我们首先打印 Error()
方法返回的错误信息。 接下来,我们尝试使用类型断言 re, ok := err.(*RequestError)
公开 RequestError
中的所有方法。 如果类型断言成功,我们再使用 Temporary()
方法来查看这个错误是否是临时错误。 由于 doRequest()
设置的 StatusCode
为 503
,匹配 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) }
我们将看到以下输出:
Outputmain: boom!
WrappedError
是一个具有两个字段的结构:一个作为 string
的上下文消息,以及一个 error
,此 WrappedError
正在提供更多信息。 当调用 Error()
方法时,我们再次使用 fmt.Sprintf
打印上下文消息,然后 error
(fmt.Sprintf
知道隐式调用 Error()
方法也是如此)。
在 main()
中,我们使用 errors.New
创建错误,然后使用我们定义的 Wrap
函数包装该错误。 这使我们能够表明这个 error
是在 "main"
中生成的。 此外,由于我们的 WrappedError
也是 error
,我们可以包装其他 WrappedError
——这将允许我们看到一个链来帮助我们追踪错误的来源. 在标准库的帮助下,我们甚至可以在错误中嵌入完整的堆栈跟踪。
结论
由于 error
接口只是一个方法,我们已经看到我们在为不同的情况提供不同类型的错误方面具有很大的灵活性。 这可以涵盖从将多条信息作为错误的一部分进行通信,一直到实现 指数退避 的所有内容。 虽然 Go 中的错误处理机制表面上看起来很简单,但我们可以使用这些自定义错误来处理常见和不常见的情况,从而实现相当丰富的处理。
Go 还有另一种传达意外行为的机制,即恐慌。 在错误处理系列的下一篇文章中,我们将研究恐慌——它们是什么以及如何处理它们。