Go 语言中集中处理 HTTP 错误

https://www.alexisbouchez.com/blog/http-error-handling-in-go

在这篇短文中,我将与你分享一个我用来集中处理 HTTP 处理程序的错误的简单模式。

如果你写过一些 Go HTTP 服务器,你可能已经厌倦了一遍又一遍地编写相同的错误处理代码:

1
2
3
4
5
6
7
8
9
10
func SomeHandler(w http.ResponseWriter, r *http.Request) {
data, err := fetchSomeData()
if err != nil {
http.Error(w, "Failed to fetch data", http.StatusInternalServerError)
log.Printf("Error fetching data: %v", err)
return
}
// More if-err blocks...
}

这段代码重复性很高,并且用样板代码塞满了你的处理程序,而不是业务逻辑。

一个更好的方法

核心思想很简单:更改你的处理程序以返回错误,而不是直接处理它们。

第一步:定义自定义 HTTP 错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package httperror
import (
"errors"
"net/http"
)
type HTTPError struct {
error
Code int
}
func New(code int, message string) *HTTPError {
return &HTTPError{
error: errors.New(message),
Code: code,
}
}
func NotFound(message string) *HTTPError {
return New(http.StatusNotFound, message)
}
// Add more helpers as needed...

第二步:创建一个处理函数包装器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Define a new handler type that returns an error
type HTTPHandlerWithErr func(http.ResponseWriter, *http.Request) error
// Handle wraps your error-returning handlers
func (r *Router) Handle(pattern string, handler HTTPHandlerWithErr) {
r.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
if err := handler(w, r); err != nil {
// Check if it's an HTTPError
var httpErr *httperror.HTTPError
if errors.As(err, &httpErr) {
http.Error(w, err.Error(), httpErr.Code)
slog.Debug("http error", "code", httpErr.Code, "err", err.Error())
} else {
// Default to 500
http.Error(w, err.Error(), http.StatusInternalServerError)
slog.Error("internal server error", "err", err.Error())
}
}
})
}

这个包装器完成了所有繁重的错误处理工作。它使用 errors.As() 来检查错误是否为 HTTPError 并提取状态码。

第三步:添加特定于方法的小助手

1
2
3
4
5
func (r *Router) Get(pattern string, handler HTTPHandlerWithErr) {
r.Handle("GET "+pattern, handler)
}
// Add Post, Put, Patch, Delete methods...

第四步:编写简洁的处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
func (c *ContainersController) Show(w http.ResponseWriter, r *http.Request) error {
id := r.PathValue("id")
container, err := c.service.FindContainer(id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return httperror.NotFound("container not found")
}
return err
}
return json.NewEncoder(w).Encode(container)
}

看看现在的处理程序多么简洁!它专注于自己的工作,而不是错误处理的杂技。

下一步是什么?

一旦你掌握了这个模式,你可以:

  1. 使用 JSON 响应:以 JSON 格式返回 API 端点的错误
  2. 添加请求 ID:在日志和响应中传递请求 ID
  3. 构建错误感知中间件:创建可与返回错误的处理器协同工作的中间件
  4. 改进错误页面:用适当的错误页面替换纯文本错误

这种模式适用于任何接受标准 Go 处理程序的router。这是一个很小的改动,但对代码质量和可维护性产生了巨大的影响。