Go Web服务的错误处理是构建稳定服务的重要基础,随着业务迭代,散落在各处的错误处理逻辑会逐渐成为代码维护的负担,通过系统性的重构可以让错误处理更规范更高效。

传统错误处理的常见问题
很多Go Web服务初期会采用直接返回错误的简单处理方式,这种方式在业务简单时问题不大,但项目规模扩大后会暴露多个问题:
- 错误响应格式不统一,不同接口返回的错误结构差异大,前端对接成本高
- 错误日志分散,没有统一的错误记录和上下文补充逻辑,排查问题时缺少关键信息
- 业务逻辑和错误处理耦合度高,大量重复的错误处理代码降低代码可读性
- 缺少错误分级机制,无法区分业务错误和系统错误,影响监控告警的准确性
重构的核心思路
优雅的错误处理重构需要围绕统一、解耦、可扩展三个目标展开,核心步骤包括定义统一错误类型、封装错误响应、实现全局错误处理中间件、补充错误上下文信息。
1. 定义统一的错误类型
首先需要定义一个自定义的错误结构体,包含错误码、错误信息、原始错误等字段,同时实现error接口,方便在函数中传递。
package errs
import "net/http"
// AppError 自定义应用错误类型
type AppError struct {
Code int // 错误码,对应HTTP状态码或业务自定义码
Message string // 对外展示的错误信息
Err error // 原始错误,用于日志记录
}
// Error 实现error接口
func (e *AppError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
// NewAppError 创建新的应用错误
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
// 预定义常见错误
var (
ErrBadRequest = NewAppError(http.StatusBadRequest, "请求参数错误", nil)
ErrUnauthorized = NewAppError(http.StatusUnauthorized, "未授权", nil)
ErrNotFound = NewAppError(http.StatusNotFound, "资源不存在", nil)
ErrInternal = NewAppError(http.StatusInternalServerError, "服务内部错误", nil)
)
2. 封装统一错误响应
为了让所有接口返回的错误格式一致,需要定义统一的响应结构,并且提供将错误转换为响应的方法。
package response
import (
"encoding/json"
"net/http"
"your_project/errs"
)
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// WriteError 将错误写入响应
func WriteError(w http.ResponseWriter, err error) {
var appErr *errs.AppError
// 判断是否为自定义应用错误
if e, ok := err.(*errs.AppError); ok {
appErr = e
} else {
// 非自定义错误归类为内部错误
appErr = errs.NewAppError(http.StatusInternalServerError, "服务内部错误", err)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(appErr.Code)
json.NewEncoder(w).Encode(Response{
Code: appErr.Code,
Message: appErr.Message,
})
}
// WriteSuccess 写入成功响应
func WriteSuccess(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Response{
Code: http.StatusOK,
Message: "success",
Data: data,
})
}
3. 实现全局错误处理中间件
通过中间件统一捕获处理函数返回的错误,避免在每个处理函数中重复编写错误响应逻辑。
package middleware
import (
"context"
"log"
"net/http"
"your_project/errs"
"your_project/response"
)
// ErrorMiddleware 全局错误处理中间件
func ErrorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 使用context传递错误,方便后续处理函数设置错误
ctx := context.WithValue(r.Context(), "error_key", nil)
r = r.WithContext(ctx)
// 捕获panic,转换为内部错误
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v, path: %s", r, r.URL.Path)
err := errs.NewAppError(http.StatusInternalServerError, "服务内部错误", nil)
response.WriteError(w, err)
}
}()
// 自定义ResponseWriter,捕获处理函数设置的错误
ew := &ErrorResponseWriter{ResponseWriter: w}
next.ServeHTTP(ew, r)
// 如果处理函数返回了错误,统一处理
if ew.err != nil {
// 记录错误日志,包含请求上下文
log.Printf("request error, path: %s, method: %s, error: %v", r.URL.Path, r.Method, ew.err)
response.WriteError(w, ew.err)
}
})
}
// ErrorResponseWriter 包装ResponseWriter,用于捕获错误
type ErrorResponseWriter struct {
http.ResponseWriter
err error
}
// SetError 设置错误
func (w *ErrorResponseWriter) SetError(err error) {
w.err = err
}
4. 业务处理函数中的错误处理
重构后,业务处理函数只需要返回自定义错误即可,不需要关心错误响应的生成逻辑。
package handler
import (
"context"
"net/http"
"your_project/errs"
"your_project/middleware"
"your_project/response"
)
// GetUser 获取用户信息的处理函数
func GetUser(w http.ResponseWriter, r *http.Request) {
// 从context中获取包装后的ResponseWriter
ew := r.Context().Value("error_key").(*middleware.ErrorResponseWriter)
userId := r.URL.Query().Get("id")
if userId == "" {
// 直接返回预定义错误,中间件会统一处理
ew.SetError(errs.ErrBadRequest)
return
}
// 模拟查询用户逻辑
user, err := queryUserFromDB(userId)
if err != nil {
// 包装原始错误,保留内部错误信息用于日志
ew.SetError(errs.NewAppError(http.StatusNotFound, "用户不存在", err))
return
}
response.WriteSuccess(w, user)
}
// 模拟查询用户函数
func queryUserFromDB(id string) (interface{}, error) {
// 模拟数据库查询错误
// return nil, fmt.Errorf("db query failed")
return map[string]string{"id": id, "name": "test user"}, nil
}
5. 路由注册与中间件使用
最后将中间件注册到路由上,确保所有请求都会经过错误处理中间件。
package main
import (
"net/http"
"your_project/handler"
"your_project/middleware"
)
func main() {
mux := http.NewServeMux()
// 注册业务路由
mux.HandleFunc("/user", handler.GetUser)
// 使用全局错误处理中间件包装路由
wrappedMux := middleware.ErrorMiddleware(mux)
// 启动服务
http.ListenAndServe(":8080", wrappedMux)
}
重构后的效果
完成上述重构后,Go Web服务的错误处理会具备以下优势:
- 所有接口的错误响应格式统一,前端对接更顺畅,减少沟通成本
- 错误处理逻辑和业务逻辑解耦,业务代码更简洁,可读性提升
- 统一的错误日志记录,包含请求上下文和原始错误信息,排查问题更高效
- 支持自定义错误扩展,后续可以增加错误告警、链路追踪等能力,可维护性更强
注意事项
在重构过程中需要注意几个细节:
- 自定义错误结构体不要过度设计,核心字段满足当前需求即可,后续可以逐步扩展
- 不要泄露敏感信息到对外错误响应中,原始错误只用于内部日志记录
- 中间件中需要正确处理panic场景,避免服务因为未捕获的panic直接崩溃
- 错误码设计需要提前规划,区分HTTP状态码和业务自定义错误码,避免混乱
GoWeb服务错误处理重构graceful_shutdown修改时间:2026-06-28 00:39:39