【go】go语言错误处理

转载的部分

这个总结的比较好,在此基础上继续总结:

  1. 使用明确的错误类型和值:定义特定领域的错误类型和值,使错误处理更加明确。

  2. 在适当的抽象层处理错误:低层函数提供详细错误信息,高层函数作出策略决策。

  3. 包装错误以保留上下文:使用 fmt.Errorf%w 添加上下文而不丢失原始错误。

  4. 使用 errors.Iserrors.As:使用这些函数检查错误类型和值,而不是直接比较。

  5. 提供结构化错误信息:错误应包含足够的信息以便调试和解决问题。

  6. 独立的错误处理逻辑:将错误处理逻辑从主要业务逻辑中分离。

  7. 避免过度使用 panic:保留 panic 用于真正的异常情况,使用 error 处理预期的错误。

  8. 适当聚合多个错误:使用错误聚合处理可能产生多个错误的操作。

  9. 考虑错误的可重试性:区分临时错误和永久错误,实现智能重试策略。

  10. 保持错误信息的一致性:在整个代码库中使用一致的错误处理风格和模式。

  • 在业务代码中使用wrap error,在基础类库中,我们不应该使用wrap error。基础库中大量自定义的error
    建议在error.New时前面用 ‘包名: 错误具体信息’,这样可以知道错误是从哪个包抛出来的,会清晰一点。

原因和理由Claude 3.7 回答如下。

基础类库与业务代码的错误处理策略

错误处理的分层策略

您提到的观点非常有价值:在业务代码中使用 wrapped errors,而基础类库需要采用不同的错误处理方式。这种分层策略有深刻的设计理念和实际考量。

基础类库中的错误处理最佳实践

1. 使用确定性错误值 (Sentinel Errors)

基础类库应该定义和返回明确的、可识别的错误值:

// 定义包级错误变量
var (
    ErrInvalidInput = errors.New("invalid input")
    ErrNotFound     = errors.New("resource not found")
    ErrPermission   = errors.New("permission denied")
)

// 在函数中返回这些预定义错误
func Parse(input string) (Result, error) {
    
    
    if !isValid(input) {
    
    
        return nil, ErrInvalidInput
    }
    // ...
}

2. 使用错误类型而非字符串拼接

为不同错误情况定义专门的错误类型:

type NotFoundError struct {
    
    
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    
    
    return fmt.Sprintf("%s with id %s not found", e.Resource, e.ID)
}

// 在函数中返回这些错误类型
func FindResource(id string) (*Resource, error) {
    
    
    // ...
    return nil, &NotFoundError{
    
    Resource: "user", ID: id}
}

3. 提供错误检查函数

为错误类型提供公共检查函数,而非依赖类型断言:

// 错误检查函数
func IsNotFound(err error) bool {
    
    
    _, ok := err.(*NotFoundError)
    return ok
}

// 客户端使用
result, err := lib.FindResource(id)
if lib.IsNotFound(err) {
    
    
    // 处理"未找到"的情况
}

4. 避免在基础类库中过度包装错误

基础类库应保持错误的原始性,避免多层包装:

// 不推荐:在库中过度包装错误
func (s *Store) Get(key string) ([]byte, error) {
    
    
    data, err := s.db.Get(key)
    if err != nil {
    
    
        return nil, fmt.Errorf("store: failed to get key %q: %w", key, err) // 过度包装
    }
    return data, nil
}

// 推荐:返回有意义的具体错误
func (s *Store) Get(key string) ([]byte, error) {
    
    
    data, err := s.db.Get(key)
    if err != nil {
    
    
        if isNotFoundError(err) {
    
    
            return nil, &KeyNotFoundError{
    
    Key: key} // 转换为领域特定错误
        }
        return nil, err // 传递底层错误或转换为库的错误类型
    }
    return data, nil
}

5. 实现支持 errors.Is/As 的错误类型

确保基础库的错误类型实现合适的方法以支持 Go 1.13+ 的错误检查:

type ValidationError struct {
    
    
    Field string
    Err   error
}

func (e *ValidationError) Error() string {
    
    
    return fmt.Sprintf("validation failed for %s: %v", e.Field, e.Err)
}

// 支持 errors.Unwrap
func (e *ValidationError) Unwrap() error {
    
    
    return e.Err
}

// 可选:支持 errors.Is 的自定义比较
func (e *ValidationError) Is(target error) bool {
    
    
    t, ok := target.(*ValidationError)
    if !ok {
    
    
        return false
    }
    return e.Field == t.Field
}

为什么基础类库和业务代码需要不同的错误处理方式?

1. API 契约与稳定性考量

基础类库需要明确的错误契约

  • 类库是被多个消费者使用的稳定 API
  • 错误类型和值构成 API 契约的一部分
  • 消费者依赖于稳定的错误类型来编写错误处理逻辑
// 类库代码 - 提供稳定的错误类型
package database

type NotConnectedError struct{
    
    }

func (e NotConnectedError) Error() string {
    
    
    return "database connection not established"
}

// 消费者代码
err := db.Query(sql)
if _, ok := err.(database.NotConnectedError); ok {
    
    
    // 重连数据库
    db.Reconnect()
}

2. 抽象级别与关注点

基础类库与业务代码处于不同的抽象层次

  • 基础类库关注特定领域错误的精确表达
  • 业务代码关注端到端流程和错误上下文链
// 基础类库 - 精确表达错误
func Validate(email string) error {
    
    
    if !emailRegex.MatchString(email) {
    
    
        return &ValidationError{
    
    Field: "email", Message: "invalid format"}
    }
    return nil
}

// 业务代码 - 添加流程上下文
func RegisterUser(user User) error {
    
    
    if err := validator.Validate(user.Email); err != nil {
    
    
        return fmt.Errorf("user registration failed: %w", err)
    }
    // ...
}

3. 复用性与上下文关联

基础类库错误需要通用复用

  • 库的错误应该与特定应用上下文无关
  • 业务代码错误通常与特定业务流程紧密关联
// 基础类库 - 通用错误
package jsonparser

var ErrSyntax = errors.New("invalid JSON syntax")

// 业务代码 - 添加特定上下文
func loadUserPreferences(userID string) (*Preferences, error) {
    
    
    data, err := readUserFile(userID)
    if err != nil {
    
    
        return nil, fmt.Errorf("loading preferences for user %s: %w", userID, err)
    }
    
    prefs, err := jsonparser.Parse(data)
    if err != nil {
    
    
        if errors.Is(err, jsonparser.ErrSyntax) {
    
    
            // 添加业务上下文
            return nil, fmt.Errorf("user %s has corrupt preferences file: %w", userID, err)
        }
        return nil, fmt.Errorf("parsing preferences for user %s: %w", userID, err)
    }
    return prefs, nil
}

4. 诊断信息与性能权衡

错误包装会增加开销

  • 基础类库通常需要考虑性能和资源使用
  • 业务代码更关注可调试性和错误诊断
// 基础类库 - 简单高效的错误
func (cache *Cache) Get(key string) (interface{
    
    }, error) {
    
    
    if !cache.initialized {
    
    
        return nil, ErrNotInitialized // 简单错误值,低开销
    }
    // ...
}

// 业务代码 - 丰富的错误上下文
func fetchUserData(userID string) (*UserData, error) {
    
    
    data, err := cache.Get(userCacheKey(userID))
    if err != nil {
    
    
        if errors.Is(err, cache.ErrNotInitialized) {
    
    
            log.Error("Cache subsystem not initialized when fetching user data")
        }
        return nil, fmt.Errorf("fetching user %s from cache: %w", userID, err) // 添加上下文
    }
    // ...
}

5. 错误处理职责划分

职责清晰分离

  • 基础类库负责提供准确的错误分类
  • 业务代码负责整合错误并添加适当上下文
// 基础类库 - 提供精确的错误类型
package paymentprocessor

type InsufficientFundsError struct {
    
    
    Account     string
    Required    float64
    Available   float64
}

func (e *InsufficientFundsError) Error() string {
    
    
    return fmt.Sprintf("insufficient funds in account %s: required %.2f, available %.2f", 
                      e.Account, e.Required, e.Available)
}

// 业务代码 - 根据错误类型做出适当反应并添加上下文
func processOrder(order Order) error {
    
    
    err := payments.Charge(order.CustomerID, order.TotalAmount)
    if err != nil {
    
    
        var fundsErr *paymentprocessor.InsufficientFundsError
        if errors.As(err, &fundsErr) {
    
    
            // 特定业务逻辑处理
            notifyCustomerAboutFunds(order.CustomerID, fundsErr.Required, fundsErr.Available)
            return fmt.Errorf("order %s payment failed due to insufficient funds: %w", 
                             order.ID, err)
        }
        return fmt.Errorf("order %s payment processing failed: %w", order.ID, err)
    }
    // ...
}

在实际项目中的应用示例:以 Kubernetes 为例

Kubernetes 作为大型Go项目,很好地展示了这种分层错误处理策略:

基础组件层(apimachinery)使用确定性错误

// kubernetes/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go
// 定义清晰的错误类型
type StatusError struct {
    
    
    ErrStatus metav1.Status
}

func (e *StatusError) Error() string {
    
    
    return e.ErrStatus.Message
}

// 提供错误检查函数
func IsNotFound(err error) bool {
    
    
    return reasonForError(err) == metav1.StatusReasonNotFound
}

func IsAlreadyExists(err error) bool {
    
    
    return reasonForError(err) == metav1.StatusReasonAlreadyExists
}

控制器层(业务逻辑)使用错误包装

// kubernetes/pkg/controller/deployment/deployment_controller.go
func (dc *DeploymentController) syncDeployment(ctx context.Context, key string) error {
    
    
    // ...
    deployment, err := dc.dLister.Deployments(namespace).Get(name)
    if err != nil {
    
    
        if apierrors.IsNotFound(err) {
    
    
            // 特定错误类型的处理
            dc.queue.Forget(key)
            return nil
        }
        return fmt.Errorf("failed to get deployment %s/%s: %w", namespace, name, err)
    }
    
    // ...
    if err := dc.scaleDeployment(deployment, newRS); err != nil {
    
    
        // 包装错误并添加业务上下文
        return fmt.Errorf("failed to scale deployment %s/%s: %w", 
                         deployment.Namespace, deployment.Name, err)
    }
    // ...
}

实践建议:基础类库错误处理的关键原则

  1. 定义清晰的错误类型和值

    • 创建表达特定错误条件的类型
    • 避免使用通用字符串错误
  2. 提供错误检查函数

    • 不要期望调用者使用类型断言
    • 为每种重要错误条件提供 IsXxx(error) bool 函数
  3. 保持错误信息简洁但完整

    • 包含必要信息,但不包含应用级别上下文
    • 确保错误消息对库的使用者有意义
  4. 实现标准接口支持

    • 实现 Unwrap() 方法支持错误链
    • 需要时提供自定义 Is()As() 方法
  5. 考虑错误的国际化和安全性

    • 避免在错误消息中包含敏感信息
    • 考虑错误消息的可翻译性
  6. 文档化错误行为

    • 在公共API文档中清晰说明可能的错误条件
    • 告知使用者如何正确检查和处理错误

总结

在软件架构中采用分层的错误处理策略是一种成熟的做法。基础类库应该提供明确的错误类型和值,专注于准确表达领域错误,而不添加应用特定上下文。业务代码则应负责包装这些错误,添加流程上下文,并决定如何对不同错误情况作出反应。

这种分层策略遵循"关注点分离"原则,使系统的不同部分能够专注于自身的核心责任,从而创建更加健壮、可维护和可演化的软件。

猜你喜欢

转载自blog.csdn.net/qq_32641095/article/details/145975226
今日推荐