Golang中私有后端API的错误处理与日志记录实践
Golang中私有后端API的错误处理与日志记录实践 大家好,
我正在开发我的第一个微服务应用,它是一个私有的后端API。 我已经开发了一些东西,但我想听听你们的建议,因为我不了解市场上常见日志聚合器的格式要求,而且我很确定这里有经验丰富的人能指出这个尝试中的许多缺陷。
为了方便测试,我在这里创建了一个仓库:
https://github.com/BigBoulard/api-error-handling-and-logging
非常感谢你们的帮助。
README
Test
go run src/main.go
Context
这段代码是一个简化的私有API示例,用于响应浏览器(客户端)的请求。
Goals
如果发生错误,客户端应该能够根据此服务器接收到的有效载荷,向最终用户提供适当的错误消息, 而不会泄露任何敏感数据,也不会透露任何关于后端代码结构或底层技术栈的信息。
错误日志记录应提供足够的信息来理解错误的来源:
Assumptions (to be challenged)
1 - 必须在控制器、服务、仓库或HTTP客户端中检测到错误时创建错误。 2 - 另一方面,日志记录应仅在控制器中进行,并应提供足够的信息来理解生成错误的请求路径。
Proposal
创建了一个 restErr 结构体,它包含:
-
提供正确HTTP错误响应所需的字段:
- HTTP状态码,
- 错误的标题
- 潜在的原因(例如缺失/无效字段的名称)。 这些字段会被序列化并呈现给客户端。
-
日志记录信息:这些数据可能包含敏感信息(数据库字段名、数据库系统名称等),因此不会被序列化。
ErrPath:包含类似productcontroller.GetProduct/productservice.GetProduct/productrepo.GetByID的内容,用于告知错误是由产品仓库的GetByID函数触发的,该函数由产品服务的GetProduct函数调用,依此类推。ErrCode和ErrMessage:从MySQL、PostGres、外部API或其他微服务检索到的原始错误代码和消息。
type restErr struct {
ErrStatus int `json:"status"` // HTTP Status Code
ErrTitle string `json:"title"` // A string representation of the Status Code
ErrCause string `json:"cause,omitempty"` // The cause of the error, can be empty
ErrPath string `json:"-"` // Only used for Logging: The path of the error. Ex: "controller/controllerfunc/service/servicefunc/dbclient/dblientfunc"
ErrMessage string `json:"-"` // Only used for Logging: Raw error message returned by a DB, another Servive or whatever
ErrCode string `json:"-"` // Only used for Logging: Raw error code from the DB or another service
}
type RestErr interface {
Status() int // HTTP status code
Title() string // A string representation of the Status Code
Path() string // Only used for Logging: The path of the error. Ex: "controller/controllerfunc/service/servicefunc/dbclient/dblientfunc"
WrapPath(string) // Only used for Logging: Wrapper func to keep track of the path of the error
Code() string // Only used for Logging: Raw error code
Message() string // Only used for Logging: Raw error message not returned to the client
Error() string // string representation of a restErr
}
更多关于Golang中私有后端API的错误处理与日志记录实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中私有后端API的错误处理与日志记录实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
错误处理与日志记录的专业分析
你的设计思路基本正确,但有几个关键问题需要改进。以下是具体的实现建议:
1. 错误接口设计问题
你的 RestErr 接口暴露了太多日志相关方法,这违反了接口隔离原则。应该将错误响应和日志记录分离:
// 客户端错误响应接口
type ClientError interface {
Status() int
Title() string
Cause() string
Error() string
}
// 日志错误接口(内部使用)
type LoggableError interface {
ClientError
Path() string
RawCode() string
RawMessage() string
WrapPath(string)
}
// 具体实现
type restErr struct {
errStatus int
errTitle string
errCause string
errPath string
errCode string
errMessage string
}
func (e *restErr) Status() int { return e.errStatus }
func (e *restErr) Title() string { return e.errTitle }
func (e *restErr) Cause() string { return e.errCause }
func (e *restErr) Path() string { return e.errPath }
func (e *restErr) RawCode() string { return e.errCode }
func (e *restErr) RawMessage() string { return e.errMessage }
func (e *restErr) WrapPath(component string) {
e.errPath = component + "/" + e.errPath
}
2. 错误工厂函数
创建统一的错误创建函数,确保错误格式一致:
func NewRestError(status int, title, cause string) *restErr {
return &restErr{
errStatus: status,
errTitle: title,
errCause: cause,
errPath: "",
}
}
func WrapError(err error, component string, code, message string) *restErr {
var restErr *restErr
if errors.As(err, &restErr) {
restErr.WrapPath(component)
if code != "" {
restErr.errCode = code
}
if message != "" {
restErr.errMessage = message
}
return restErr
}
// 如果是普通error,转换为restErr
return &restErr{
errStatus: http.StatusInternalServerError,
errTitle: "Internal Server Error",
errCause: err.Error(),
errPath: component,
errCode: code,
errMessage: message,
}
}
3. 中间件日志记录
在控制器层面统一记录日志,而不是在每个控制器方法中:
func LoggingMiddleware(logger *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 创建自定义ResponseWriter来捕获状态码
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
duration := time.Since(start)
// 记录请求日志
logger.Info("request completed",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.Int("status", rw.statusCode),
zap.Duration("duration", duration),
zap.String("user_agent", r.UserAgent()),
)
})
}
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
4. 错误响应格式化
统一错误响应格式,确保不泄露敏感信息:
func WriteError(w http.ResponseWriter, err error) {
var clientErr ClientError
if errors.As(err, &clientErr) {
response := map[string]interface{}{
"status": clientErr.Status(),
"title": clientErr.Title(),
"cause": clientErr.Cause(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(clientErr.Status())
json.NewEncoder(w).Encode(response)
// 如果是可记录的错误,记录详细信息
var logErr LoggableError
if errors.As(err, &logErr) {
zap.L().Error("request error",
zap.String("path", logErr.Path()),
zap.String("code", logErr.RawCode()),
zap.String("message", logErr.RawMessage()),
zap.Int("http_status", logErr.Status()),
)
}
return
}
// 未知错误,返回通用错误
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "500",
"title": "Internal Server Error",
"cause": "An unexpected error occurred",
})
}
5. 使用示例
// 仓库层
func (r *ProductRepo) GetByID(id string) (*Product, error) {
var product Product
err := r.db.QueryRow("SELECT * FROM products WHERE id = $1", id).Scan(&product)
if err != nil {
if err == sql.ErrNoRows {
return nil, NewRestError(http.StatusNotFound, "Not Found", "Product not found")
}
// 包装数据库错误
return nil, WrapError(err, "productrepo.GetByID", "DB_ERROR", err.Error())
}
return &product, nil
}
// 服务层
func (s *ProductService) GetProduct(id string) (*Product, error) {
product, err := s.repo.GetByID(id)
if err != nil {
return nil, WrapError(err, "productservice.GetProduct", "", "")
}
return product, nil
}
// 控制器层
func (c *ProductController) GetProduct(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
product, err := c.service.GetProduct(id)
if err != nil {
WriteError(w, WrapError(err, "productcontroller.GetProduct", "", ""))
return
}
json.NewEncoder(w).Encode(product)
}
6. 日志聚合器兼容性
对于常见的日志聚合器(如ELK、Loki、Datadog),建议使用结构化日志格式:
// 初始化Zap日志
func NewLogger() *zap.Logger {
config := zap.NewProductionConfig()
config.OutputPaths = []string{"stdout"}
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
return logger
}
// 日志格式示例(JSON):
// {
// "timestamp": "2024-01-15T10:30:00Z",
// "level": "error",
// "msg": "request error",
// "path": "productcontroller.GetProduct/productservice.GetProduct/productrepo.GetByID",
// "code": "DB_ERROR",
// "message": "connection refused",
// "http_status": 500,
// "trace_id": "abc-123-xyz" // 分布式追踪ID
// }
关键改进点:
- 接口分离:将客户端错误和日志错误接口分离
- 错误包装:使用
WrapError统一包装错误,保持调用链 - 中间件日志:在中间件中统一记录请求日志,避免控制器污染
- 结构化日志:使用Zap等结构化日志库,方便日志聚合器解析
- 错误工厂:统一错误创建方式,确保一致性
这种设计符合微服务错误处理的最佳实践,同时满足日志聚合器的格式要求。

