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向用户提供错误信息(例如:使用错误代码如NoSuchUser、TooManyRequest)。但我们不希望用户知道内部错误,如ConnectionError、DiskError等。我们希望向用户隐藏内部错误,只显示InternalServerError。
那么我们的user.Service实现应该如何返回错误,以便我们的API可以决定是否将该错误隐藏为InternalServerError。
我发现Dave Cheney写的一篇很好的文章《不要只是检查错误,要优雅地处理错误》。 文章说我们应该通过行为来断言错误,而不是通过类型。
我们是否应该创建接口IsInternalError,这样API就可以知道是否应该隐藏错误。但是user.Service的实现如何知道它应该返回实现此接口的错误呢?
更多关于Golang中如何优雅地隐藏API错误的实战教程也可以访问 https://www.itying.com/category-94-b0.html
但是,如果我们不希望 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)
是的,我最初也是这么想的。然后我读了这篇文章:不要只是检查错误,要优雅地处理它们。文章指出我们应该通过错误的行为来断言错误,而不是通过其类型。
在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)
}
这种方法的优势:
- 清晰的错误分类:内部错误和业务错误明确分离
- 类型安全:使用Go的错误检查和类型断言机制
- 可扩展:可以轻松添加新的错误类型
- 符合Go习惯:遵循Go的错误处理最佳实践
通过这种方式,API层可以准确判断哪些错误需要暴露给用户,哪些应该隐藏为通用内部错误,同时保持代码的清晰和可维护性。


