API 设计:错误处理
错误是创建API时最容易忽略的事。用户会时不时遇到问题,错误是他们最先看到的东西。在上面花时间是值得的,这样可以使API有更好的用户体验。
› 指导原则
好的错误消息应该遵循下面几点:
- 说明哪里出错了。
- 解释你能做什么。
- 对于可恢复的错误,易于隔离和处理。
› 案例研究
我们将重新使用前文的HTTP客户端案例,其 API 表层如下所示:
package http
import (
"io"
)
type Response struct {
StatusCode int
Headers map[string]string
Body io.ReadCloser
}
type options struct {}
type Option = func(*options)
var (
FollowRedirects = func(o *options) {}
Header = func(key, value string) func(*options) {}
)
func Get(url string, options ...Option) (Response, error) {}
下面是一个实际调用的例子:
package main
import (
"fmt"
"http"
"io"
"os"
)
func main() {
res, err := http.Get("https://example.com", http.FollowRedirects, http.Header("Accept-Encoding", "gzip"))
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if res.StatusCode != 200 {
fmt.Fprintf(os.Stderr, "non-200 status code: %v\n", res.StatusCode)
os.Exit(1)
}
_, err := io.Copy(os.Stdout, res.Body)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if err := res.Body.Close(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
需要澄清的是,该原则适用于所有语言,不仅限于 Go。
› 有用的错误信息
当返回错误时,首先需要区分的是调用者是否可以对此采取任何措施。
以网络错误为例。以下是网络连接程序正常操作的全部内容:
- 目标进程已崩溃,正在启动备份。
- 您和目的地之间的节点出现故障,没有转发任何数据包。
- 目标进程过载,正在限制客户端的速率以帮助恢复。
以下是发生上述情况时我希望看到的内容:
HTTP GET request to https://example.com failed with error: connection refused, ECONNREFUSED. Run man 2 connect for more information. You can pass http.NumRetries(int) as an option, but keep in mind that retrying too much can get you rate limited or blacklisted from some sites.
这让我免于在线挖掘、查阅文档,以及愤怒的网站管理员或自动限速器的一记耳光。它非常符合我们指导原则中的第1点和第2点。
不过,这不是您想要向最终用户显示的内容。我们需要为API用户提供处理错误的工具(比如为他们提供重试选项),或者向最终用户显示一条漂亮的错误消息。
› 不同类型的错误
不同语言对于错误类型的区分有不同的最佳实践。我们将学习Go、Java、Ruby和Python。
› Go
在Go中执行此操作的惯用方法是导出API中的函数,这些函数可以检查抛出错误的属性。
package http
type httpError struct {
retryable bool
}
func IsRetryable(err error) bool {
httpError, ok := err.(httpError)
return ok && httpError.retryable
}
然后,这样使用:
package main
func main() {
res, err := http.Get("https://example.com")
if err != nil {
if http.IsRetryable(err) {
// retry
} else {
// bail
}
}
这个想法扩展到错误可能具有的任何属性。任何您觉得API用户可能想做决定的内容都应该以这种方式暴露。
› Java
在Java中实现这一点的机制更为明确:自定义异常类型。
public class HttpException extends Exception {
private final boolean retryable;
private HttpException(String msg, Throwable cause, boolean retryable) {
super(msg, cause);
this.retryable = retryable;
}
public boolean isRetryable() {
return this.retryable;
}
}
这样使用它:
public final class Main {
public static void main(String... args) {
Response res;
try {
res = Http.get("https://example.com");
} catch (HttpException e) {
if (e.isRetryable()) {
// retry
} else {
// bail
}
}
}
}
› Python
Python中的情况类似。
class Error(Exception):
pass
class HttpError(Error):
def __init__(self, message, retryable):
self.message = message
self.retryable = retryable
def is_retryable(self):
return self.retryable
And using it:
try:
res = Http.get("https://example.com")
except HttpError as err:
if err.is_retryable():
# retry
else:
# bail
编写从Exception扩展的通用Error类是编写Python库时的常见做法。它允许API的用户根据自己的意愿编写catch-all错误处理。
› Ruby
然后是 Ruby。
class HttpError < StandardError
def initialize message, retryable
@retryable = retryable
super(message)
end
def is_retryable
@retryable
end
end
这样使用它:
begin
res = Http.get("https://example.com")
rescue HttpError => e
if e.is_retryable
# retry
else
# bail
和 Python 基本相同。
› 结语
不要忽视您的错误消息。他们通常是用户与你编码的第一次接触,如果这很糟糕,他们会感到沮丧。
您希望让代码的用户能够灵活地以对他们最有意义的方式处理错误。使用上述方法,尽可能多地向他们提供有关情况的信息。
我有心去接触Rust和函数式语言,不过还没有实际接触。他们的错误处理方法与上述方法有很大不同。如果你知道其他语言中的好模式,我很乐意在评论中听到它们。