在微服务架构的Golang项目中,服务间调用产生的错误如果分散在各个业务函数中单独处理,会出现大量重复的错误处理逻辑,比如错误码转换、错误信息封装、错误日志上报等操作。统一处理微服务调用错误可以把这些通用逻辑收敛,减少冗余代码,也方便后续统一调整错误处理的规则。
统一错误处理的核心设计思路
要实现统一的微服务调用错误处理,需要先明确几个核心设计点:
- 定义统一的错误结构体,包含错误码、错误信息、原始错误等字段,方便不同模块传递错误上下文
- 封装微服务调用的通用客户端,在客户端层统一捕获调用过程中产生的错误,进行标准化转换
- 设计错误码规范,区分不同场景的错误类型,比如网络错误、服务不可用、业务校验失败等
- 提供错误处理的工具函数,方便业务层快速判断错误类型、提取错误信息
定义统一的错误类型
首先我们需要定义一个标准的错误结构体,所有微服务调用产生的错误都会转换为该结构体的实例,这样后续的处理逻辑可以基于统一的结构进行。
package errcode
// 定义错误结构体
type AppError struct {
Code int // 错误码,0表示成功,非0表示失败
Message string // 错误提示信息
Err error // 原始错误,用于记录底层错误信息
}
// 实现error接口
func (e *AppError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
// 预定义通用错误码
const (
SuccessCode = 0
NetErrorCode = 1001 // 网络调用错误
ServiceUnavailableCode = 1002 // 服务不可用
BusinessErrorCode = 2001 // 业务校验错误
)
// 快速创建错误实例的工具函数
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
封装统一的微服务调用客户端
我们可以把微服务调用的通用逻辑封装到一个客户端结构体中,在调用远程服务的时候统一捕获错误,转换为我们定义的标准错误类型。这里以HTTP形式的微服务调用为例:
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"your_project/errcode"
)
// 微服务调用客户端
type MicroServiceClient struct {
baseURL string
httpClient *http.Client
}
// 初始化客户端
func NewMicroServiceClient(baseURL string) *MicroServiceClient {
return &MicroServiceClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 3 * time.Second, // 设置默认超时时间
},
}
}
// 统一的调用方法,所有微服务调用都通过该方法来执行
func (c *MicroServiceClient) Call(method, path string, reqBody interface{}, respBody interface{}) *errcode.AppError {
// 序列化请求体
var reqBytes []byte
var err error
if reqBody != nil {
reqBytes, err = json.Marshal(reqBody)
if err != nil {
return errcode.NewAppError(errcode.NetErrorCode, "请求参数序列化失败", err)
}
}
// 构造请求
url := c.baseURL + path
req, err := http.NewRequest(method, url, bytes.NewReader(reqBytes))
if err != nil {
return errcode.NewAppError(errcode.NetErrorCode, "构造请求失败", err)
}
req.Header.Set("Content-Type", "application/json")
// 执行请求
resp, err := c.httpClient.Do(req)
if err != nil {
return errcode.NewAppError(errcode.ServiceUnavailableCode, "调用远程服务失败", err)
}
defer resp.Body.Close()
// 读取响应体
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return errcode.NewAppError(errcode.NetErrorCode, "读取响应失败", err)
}
// 校验响应状态码
if resp.StatusCode != http.StatusOK {
return errcode.NewAppError(errcode.BusinessErrorCode, fmt.Sprintf("服务返回错误状态码: %d", resp.StatusCode), nil)
}
// 反序列化响应体
if respBody != nil {
err = json.Unmarshal(respBytes, respBody)
if err != nil {
return errcode.NewAppError(errcode.NetErrorCode, "响应参数反序列化失败", err)
}
}
return nil
}
业务层使用统一错误处理
封装好客户端之后,业务层调用微服务的时候就不需要再单独处理错误转换逻辑,只需要判断返回的错误是否为空即可,同时可以基于错误码做后续的业务处理。
package service
import (
"fmt"
"your_project/client"
"your_project/errcode"
)
// 定义用户服务的响应结构体
type UserInfoResp struct {
UserID int `json:"user_id"`
UserName string `json:"user_name"`
}
// 获取用户信息的业务逻辑
func GetUserInfo(userID int) (*UserInfoResp, *errcode.AppError) {
// 初始化用户服务的调用客户端
userClient := client.NewMicroServiceClient("http://127.0.0.1:8081")
var resp UserInfoResp
// 调用用户服务的接口
err := userClient.Call("GET", fmt.Sprintf("/user/info?user_id=%d", userID), nil, &resp)
if err != nil {
// 统一处理错误,这里可以根据错误码做不同的逻辑
if err.Code == errcode.ServiceUnavailableCode {
// 服务不可用,可以走降级逻辑
fmt.Println("用户服务不可用,返回默认用户信息")
return &UserInfoResp{UserID: userID, UserName: "默认用户"}, nil
}
return nil, err
}
return &resp, nil
}
中间件层统一拦截错误
如果项目使用了Gin、Echo等Web框架,还可以通过中间件的方式在请求返回前统一拦截错误,进行日志上报、错误响应格式统一等操作。这里以Gin框架为例:
package middleware
import (
"net/http"
"your_project/errcode"
"github.com/gin-gonic/gin"
)
// 统一错误处理中间件
func ErrorHandlerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// 获取上下文中存储的错误
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
// 判断是否为我们定义的标准错误类型
if appErr, ok := err.(*errcode.AppError); ok {
c.JSON(http.StatusOK, gin.H{
"code": appErr.Code,
"message": appErr.Message,
"data": nil,
})
return
}
// 其他类型的错误,返回通用错误
c.JSON(http.StatusOK, gin.H{
"code": 500,
"message": "服务内部错误",
"data": nil,
})
}
}
}
注意事项
在实际落地统一错误处理的时候,还需要注意几个问题:
- 错误码的设计要预留扩展空间,避免后续新增错误类型时没有合适的编码可用
- 原始错误的存储要注意不要泄露敏感信息,比如数据库连接错误不要直接返回给用户
- 超时时间、重试策略等调用配置可以放在客户端结构体中,支持不同服务配置不同的参数
- 如果使用了gRPC等其他的微服务通信方式,只需要调整客户端的调用逻辑,错误转换部分可以复用