告别混乱的错误处理:Uber Go错误处理规范实战指南

告别混乱的错误处理:Uber Go错误处理规范实战指南

【免费下载链接】uber_go_guide_cn Uber Go 语言编码规范中文版. The Uber Go Style Guide . 【免费下载链接】uber_go_guide_cn 项目地址: https://gitcode.com/gh_mirrors/ub/uber_go_guide_cn

你还在为Go项目中的错误处理感到头疼吗?错误信息模糊不清、错误类型难以识别、错误链冗长混乱?本文将带你深入理解Uber Go错误处理规范,从错误命名、类型定义到错误包装,构建一套健壮的错误处理体系,让你的代码更可靠、调试更高效。读完本文,你将掌握错误处理的最佳实践,学会如何编写清晰、可维护的错误处理代码。

错误处理的重要性

在软件开发中,错误处理是保证系统稳定性和可靠性的关键环节。一个健壮的错误处理体系能够帮助开发者快速定位问题、优雅地处理异常情况,提升用户体验。Uber作为全球领先的科技公司,在Go语言实践中积累了丰富的经验,其错误处理规范为我们提供了宝贵的参考。

错误命名规范

错误命名是错误处理的基础,良好的命名能够直观地反映错误的性质和来源。Uber Go规范中对错误命名有明确的规定:对于存储为全局变量的错误值,根据是否导出,使用前缀Errerr

全局错误变量命名

导出的错误变量使用Err前缀,未导出的使用err前缀。这样可以清晰地区分错误的可访问性,便于调用者判断是否可以直接匹配和处理该错误。

var (
    // 导出的错误变量,供调用者使用errors.Is匹配
    ErrBrokenLink = errors.New("link is broken")
    ErrCouldNotOpen = errors.New("could not open")

    // 未导出的错误变量,仅包内使用
    errNotFound = errors.New("not found")
)

自定义错误类型命名

对于自定义错误类型,应使用Error作为后缀,如NotFoundError。这样可以明确该类型的用途,提高代码的可读性。

// 导出的自定义错误类型
type NotFoundError struct {
    File string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("file %q not found", e.File)
}

// 未导出的自定义错误类型
type resolveError struct {
    Path string
}

func (e *resolveError) Error() string {
    return fmt.Sprintf("resolve %q", e.Path)
}

详细规范可参考error-name.md

错误类型选择

Go语言提供了多种错误声明方式,选择合适的错误类型对于错误处理至关重要。Uber Go规范总结了不同场景下的错误类型选择策略,主要考虑两个因素:调用者是否需要匹配错误,以及错误消息是静态还是动态。

错误类型选择决策表

根据错误匹配需求和消息类型,Uber Go规范给出了以下决策指南:

错误匹配?错误消息推荐方案
静态[errors.New]
动态[fmt.Errorf]
静态顶层var变量 + [errors.New]
动态自定义error类型

不同错误类型的应用场景

  1. 无需匹配的静态错误:使用errors.New创建简单的静态错误消息。这种错误通常用于不需要调用者特殊处理的情况。
func Open() error {
    return errors.New("could not open")
}
  1. 无需匹配的动态错误:使用fmt.Errorf创建包含动态信息的错误消息。适用于错误消息需要根据上下文变化,但调用者无需区分具体错误类型的场景。
func Open(file string) error {
    return fmt.Errorf("file %q not found", file)
}
  1. 需要匹配的静态错误:将错误声明为顶层变量,使用errors.New初始化。这样调用者可以使用errors.Is进行匹配和处理。
var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
    return ErrCouldNotOpen
}

// 调用者代码
if err := Open(); err != nil {
    if errors.Is(err, ErrCouldNotOpen) {
        // 处理特定错误
    } else {
        panic("unknown error")
    }
}
  1. 需要匹配的动态错误:定义自定义错误类型,包含必要的上下文信息。调用者可以使用errors.As提取错误类型并处理。
type NotFoundError struct {
    File string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
    return &NotFoundError{File: file}
}

// 调用者代码
if err := Open("testfile.txt"); err != nil {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        // 处理特定错误
        fmt.Printf("File not found: %s\n", notFound.File)
    } else {
        panic("unknown error")
    }
}

详细规范可参考error-type.md

错误包装与上下文添加

当函数调用失败时,如何传播错误是构建清晰错误链的关键。Uber Go规范定义了三种主要的错误传播方式:原样返回原始错误、使用fmt.Errorf%w动词添加上下文、使用fmt.Errorf%v动词添加上下文。

错误传播策略

  1. 原样返回原始错误:如果没有额外上下文可添加,直接返回原始错误。这保持了原始错误类型和消息,适用于底层错误消息已包含足够信息的情况。

  2. 使用%w包装错误:当调用者需要访问底层错误时,使用%w动词包装错误。这是大多数包装错误的默认选择,但要注意调用者可能会依赖这种行为,因此对于已知的错误变量或类型,应将其作为函数契约的一部分进行文档化和测试。

  3. 使用%v添加上下文:使用%v动词可以模糊底层错误,使调用者无法匹配原始错误。如果未来需要允许调用者匹配,可以切换到%w

添加上下文的最佳实践

添加错误上下文时,应保持简洁,避免使用"failed to"等冗余短语,因为当错误沿调用栈向上传播时,这些短语会累积,导致错误消息冗长。

不推荐的方式

s, err := store.New()
if err != nil {
    return fmt.Errorf("failed to create new store: %w", err)
}

推荐的方式

s, err := store.New()
if err != nil {
    return fmt.Errorf("new store: %w", err)
}

对比错误消息:

  • 不推荐:failed to x: failed to y: failed to create new store: the error
  • 推荐:x: y: new store: the error

详细规范可参考error-wrap.md

错误处理原则:一次处理

错误处理的一个重要原则是"一次处理"。当调用者从被调用者接收到错误时,可以根据对错误的了解采取多种处理方式,但通常应只处理每个错误一次。调用者不应既记录错误又返回错误,因为其调用者可能也会处理该错误,导致重复记录。

错误处理的常见场景

  1. 不推荐:记录并返回错误

这种方式会导致错误在调用栈中被多次记录,造成日志冗余。

u, err := getUser(id)
if err != nil {
    // 不推荐:记录错误后又返回,可能导致重复记录
    log.Printf("Could not get user %q: %v", id, err)
    return err
}
  1. 推荐:包装并返回错误

将错误包装后返回,让上层调用者处理。使用%w确保调用者可以使用errors.Iserrors.As匹配错误。

u, err := getUser(id)
if err != nil {
    return fmt.Errorf("get user %q: %w", id, err)
}
  1. 推荐:记录并优雅降级

如果操作不是严格必要的,可以记录错误并提供降级但未中断的体验。

if err := emitMetrics(); err != nil {
    // 写入指标失败不应中断应用
    log.Printf("Could not emit metrics: %v", err)
}
  1. 推荐:匹配错误并优雅降级

如果被调用者定义了特定错误,且失败是可恢复的,可以匹配该错误并优雅降级。对于其他情况,包装错误并返回。

tz, err := getUserTimeZone(id)
if err != nil {
    if errors.Is(err, ErrUserNotFound) {
        // 用户不存在,使用UTC时区
        tz = time.UTC
    } else {
        return fmt.Errorf("get user %q: %w", id, err)
    }
}

详细规范可参考error-once.md

错误处理最佳实践总结

Uber Go错误处理规范为我们提供了一套全面的指导,遵循这些规范可以帮助我们构建更健壮、更易维护的错误处理体系。以下是关键要点的总结:

  1. 一致的错误命名:使用Err/err前缀命名错误变量,使用Error后缀命名自定义错误类型。
  2. 合适的错误类型选择:根据错误匹配需求和消息类型选择合适的错误声明方式。
  3. 清晰的错误包装:使用%w%v添加有意义的上下文,避免冗余信息。
  4. 一次错误处理:每个错误只处理一次,避免重复记录或处理。

通过遵循这些规范,我们的Go代码将具有更好的可读性、可维护性和可靠性,错误处理将不再是项目中的痛点,而是提升代码质量的助力。

希望本文对你理解和应用Uber Go错误处理规范有所帮助。如果你有任何疑问或建议,欢迎在项目仓库中提出issue或PR。

【免费下载链接】uber_go_guide_cn Uber Go 语言编码规范中文版. The Uber Go Style Guide . 【免费下载链接】uber_go_guide_cn 项目地址: https://gitcode.com/gh_mirrors/ub/uber_go_guide_cn

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值