Golang中如何优雅地隐藏API错误

Golang中如何优雅地隐藏API错误 如何处理来自API的错误?

假设我们创建一个暴露getUserById功能的服务器。有多种选项可用于暴露我们的功能(例如:使用grpc/rest/graphql)。也有多种选项来持久化数据(例如:mysql、redis,或者仅使用内存?)。假设我有以下接口:

package user

type Service interface {
    UserByID(ID: string): (*User, error)
}

然后我们可以使用mysql/sqlite/inmemory来实现它。但是所有这些实现都有不同的行为,并可能返回不同的错误。Mysql可能返回连接错误,sqlite可能返回指示磁盘空间不足的错误。

当用户调用我们的API时,可能会经过graphql -> user.Service -> 可能是user.Store?。当发生错误时,我们希望我们的API向用户提供错误信息(例如:使用错误代码如NoSuchUserTooManyRequest)。但我们不希望用户知道内部错误,如ConnectionErrorDiskError等。我们希望向用户隐藏内部错误,只显示InternalServerError

那么我们的user.Service实现应该如何返回错误,以便我们的API可以决定是否将该错误隐藏为InternalServerError

我发现Dave Cheney写的一篇很好的文章《不要只是检查错误,要优雅地处理错误》。 文章说我们应该通过行为来断言错误,而不是通过类型。

我们是否应该创建接口IsInternalError,这样API就可以知道是否应该隐藏错误。但是user.Service的实现如何知道它应该返回实现此接口的错误呢?


更多关于Golang中如何优雅地隐藏API错误的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

但是,如果我们不希望 user.Service 的实现依赖于所使用的技术,该怎么办?我们能否创建一个松散耦合的 user.Service 实现,使其不关心我们使用的是 HTTP、TCP 还是其他协议?

更多关于Golang中如何优雅地隐藏API错误的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


假设您正在处理HTTP相关的工作,可以简单地返回一个包含错误代码和说明的JSON,甚至是更复杂的结构。另一种简单的方法是在响应头中返回纯文本错误信息和相应的HTTP状态码,例如:

http.Error(w, "Session expired", http.StatusNetworkAuthenticationRequired)

是的,您可以创建自定义错误。

图片

创建错误的3种简单方法

代码示例 如何创建简单的基于字符串的错误和带有数据的自定义错误类型。

是的,我最初也是这么想的。然后我读了这篇文章:不要只是检查错误,要优雅地处理它们。文章指出我们应该通过错误的行为来断言错误,而不是通过其类型。

在Go中优雅地隐藏API错误的关键是使用自定义错误类型和错误包装机制。通过定义明确的错误类型,API层可以判断哪些错误需要暴露给用户,哪些应该隐藏为内部错误。

以下是一个完整的实现示例:

package user

import (
    "errors"
    "fmt"
)

// 定义可暴露给用户的错误类型
var (
    ErrNoSuchUser     = errors.New("no such user")
    ErrTooManyRequest = errors.New("too many requests")
    ErrInternal       = errors.New("internal server error")
)

// 内部错误类型 - 不暴露给用户
type internalError struct {
    cause error
    msg   string
}

func (e *internalError) Error() string {
    return fmt.Sprintf("%s: %v", e.msg, e.cause)
}

func (e *internalError) Unwrap() error {
    return e.cause
}

func (e *internalError) IsInternal() bool {
    return true
}

// InternalError 接口用于检查是否为内部错误
type InternalError interface {
    error
    IsInternal() bool
}

// 创建内部错误的辅助函数
func NewInternalError(cause error, msg string) error {
    return &internalError{
        cause: cause,
        msg:   msg,
    }
}

// 检查错误是否为内部错误
func IsInternalError(err error) bool {
    var internalErr InternalError
    return errors.As(err, &internalErr) && internalErr.IsInternal()
}

// Service 实现示例
type ServiceImpl struct {
    store Store
}

func (s *ServiceImpl) UserByID(id string) (*User, error) {
    user, err := s.store.GetUserByID(id)
    if err != nil {
        // 将底层存储错误转换为适当的错误类型
        if errors.Is(err, ErrUserNotFound) {
            return nil, ErrNoSuchUser
        }
        
        // 数据库连接错误、磁盘空间不足等转换为内部错误
        if isDatabaseError(err) {
            return nil, NewInternalError(err, "database operation failed")
        }
        
        // 其他未知错误也作为内部错误
        return nil, NewInternalError(err, "unknown error occurred")
    }
    
    return user, nil
}

// API层错误处理
func handleGetUserRequest(userID string) (interface{}, error) {
    user, err := userService.UserByID(userID)
    if err != nil {
        if IsInternalError(err) {
            // 对用户隐藏内部错误细节
            return nil, ErrInternal
        }
        // 暴露业务逻辑错误给用户
        return nil, err
    }
    
    return user, nil
}

// 存储层错误定义示例
var ErrUserNotFound = errors.New("user not found in storage")

func isDatabaseError(err error) bool {
    // 这里可以根据实际使用的数据库库来判断
    // 例如检查错误是否包含特定字符串或类型
    return err != nil && 
           (contains(err.Error(), "connection") || 
            contains(err.Error(), "disk") ||
            contains(err.Error(), "timeout"))
}

func contains(s, substr string) bool {
    return len(s) >= len(substr) && 
           (s == substr || len(s) > len(substr) && 
            (s[0:len(substr)] == substr || 
             contains(s[1:], substr)))
}

在GraphQL或REST API层中的使用示例:

// GraphQL解析器示例
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
    result, err := handleGetUserRequest(id)
    if err != nil {
        // 这里可以根据错误类型返回不同的HTTP状态码
        if errors.Is(err, ErrNoSuchUser) {
            return nil, &gqlerror.Error{
                Message: "User not found",
                Extensions: map[string]interface{}{
                    "code": "NOT_FOUND",
                },
            }
        }
        if errors.Is(err, ErrInternal) {
            return nil, &gqlerror.Error{
                Message: "Internal server error",
                Extensions: map[string]interface{}{
                    "code": "INTERNAL_ERROR",
                },
            }
        }
    }
    
    return result.(*User), nil
}

// REST API处理示例
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.URL.Query().Get("id")
    result, err := handleGetUserRequest(userID)
    
    if err != nil {
        switch {
        case errors.Is(err, ErrNoSuchUser):
            writeJSONError(w, http.StatusNotFound, "user not found")
        case errors.Is(err, ErrTooManyRequest):
            writeJSONError(w, http.StatusTooManyRequests, "too many requests")
        case errors.Is(err, ErrInternal):
            writeJSONError(w, http.StatusInternalServerError, "internal server error")
        default:
            writeJSONError(w, http.StatusInternalServerError, "internal server error")
        }
        return
    }
    
    writeJSONResponse(w, http.StatusOK, result)
}

这种方法的优势:

  1. 清晰的错误分类:内部错误和业务错误明确分离
  2. 类型安全:使用Go的错误检查和类型断言机制
  3. 可扩展:可以轻松添加新的错误类型
  4. 符合Go习惯:遵循Go的错误处理最佳实践

通过这种方式,API层可以准确判断哪些错误需要暴露给用户,哪些应该隐藏为通用内部错误,同时保持代码的清晰和可维护性。

回到顶部