Go 1.20中fmt.Errorf的更新

https://www.zhihu.com/question/36444352

Go 的错误处理是“积类型”,而 Rust 是“和类型”。这是最本质的区别。


在1.20之前的版本,fmt.Errorf后面只能格式化一个%w,两个的话会不符合预期,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"errors"
"fmt"
"strconv"
)

func main() {

_, err1 := strconv.Atoi("1a")

err2 := errors.New("new error")

rs := fmt.Errorf("err1 is %w; err2 is %w", err1, err2)

fmt.Println(rs)
}

err1 is strconv.Atoi: parsing "1a": invalid syntax; err2 is %!w(*errors.errorString=&{new error})


用 Go 1.20及以上版本:

err1 is strconv.Atoi: parsing "1a": invalid syntax; err2 is new error


相关

术语从 “错误链” 修改为 “错误树”。配套方法 errors.Is、errors.As、fmt.Errorf 都进行了改造。

对应如下:

  • errors.Is:如果能够匹配上任何错误,则返回 true。
  • errors.As:返回第一个匹配的错误。
  • fmt.Errorf:将会把多个错误封装在用户定义的布局中。

新增了一个 errors.Join

Go1.20 继续小修小补 errors 库

Go 1.20新特性前瞻-支持wrap multiple errors

8千字详解Go1.20稳定版





20240818

012-go语言errors(上)改进历史和用法

我将按照您的要求整理内容,保持原有信息不遗漏,并使其更加合理通顺。以下是整理后的内容:

Go 语言 errors 包的改进历史和用法(上)

Go 语言 errors 包的发展历程:

  1. 2012年:Go 1.0 发布,error 已经是接口实现。

  2. 2019年:Go 1.13 版本发布,新增功能:

    • errors.Is 函数
    • errors.As 函数
    • 解包函数
    • 装包函数
  3. 2023年:新增 errors.Join 函数,作用是将多个错误合成一个错误。新增解包接口返回 slice 类型。

Go 1.13 之前的错误处理:

以 HTTP Get 函数返回中获取 syscall.ECONNREFUSED 错误为例:

1
2
3
4
5
6
7
8
9
10
11
if err != nil {
if urlErr, ok := err.(*url.Error); ok {
if opErr, ok := urlErr.Err.(*net.OpError); ok {
if syscallErr, ok := opErr.Err.(*os.SyscallError); ok {
if syscallErr.Err == syscall.ECONNREFUSED {
// 处理连接被拒绝的错误
}
}
}
}
}

这种方法的局限性:

  • 需要了解错误的完整路径
  • 类似于目录查找,需要知道绝对路径
  • 心智成本高

Go 1.13 版本的改进:

使用 errors.Is 函数:

1
2
3
if errors.Is(err, syscall.ECONNREFUSED) {
// 处理连接被拒绝的错误
}

改进点:

  • 只需知道具体错误的名字
  • 可以直接判断是否存在这个错误
  • 心智成本大幅降低

错误分类的改进:

Go 1.13 之前的做法:

1
2
3
4
5
err := fmt.Errorf("调用标准库发生错误: %v", stdLibErr)
// 使用时
if strings.Contains(err.Error(), "某些特定字符串") {
// 处理特定错误
}

缺点:

  • 丢失结构化信息
  • 只能通过字符串方式分类
  • 对上游依赖脆弱,容错性差

Go 1.13 之后的做法:

1
2
3
4
5
6
7
8
9
10
11
12
var ErrNotFound = errors.New("not found")
var ErrAlreadyExists = errors.New("already exists")

func someFunction() error {
// ...
return fmt.Errorf("操作失败: %w", ErrNotFound)
}

// 使用时
if errors.Is(err, ErrNotFound) {
// 处理 not found 错误
}

优点:

  • 错误包装不会丢失结构化信息
  • 形成错误链表

实际应用示例:

定义错误结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
type ErrMsg struct {
Code int
Message string
}

func (e *ErrMsg) Error() string {
return fmt.Sprintf("错误码:%d,错误信息:%s", e.Code, e.Message)
}

var (
ErrNotFound = &ErrMsg{Code: 404, Message: "未找到"}
ErrAlreadyExists = &ErrMsg{Code: 409, Message: "已存在"}
)

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func getUserInfo(id string) (string, error) {
// 模拟 RPC 调用
info, err := redis.Get(id)
if err != nil {
return "", fmt.Errorf("获取用户信息失败: %w", fmt.Errorf("Redis错误: %w", ErrNotFound))
}
return info, nil
}

func processCodeMsg(err error) (int, string) {
if err == nil {
return 0, ""
}
var errMsg *ErrMsg
if errors.As(err, &errMsg) {
return errMsg.Code, errMsg.Message
}
return 500, "未知错误"
}

Go 1.13 带来的主要改进:

  1. errors.Is:在错误链中查找特定错误
  2. errors.As:在错误链中查找特定类型的错误并提取
  3. fmt.Errorf 的 %w:对错误进行包装
  4. errors.Unwrap:对错误进行解包(每次只能解包一次,使用较少)

错误合并:

  1. 使用 fmt.Errorf:
1
err3 := fmt.Errorf("错误3: %w %w", err1, err2)
  1. 使用 errors.Join(Go 1.20 新增):
1
err3 := errors.Join(err1, err2)

性能对比:

  • errors.Join:每次操作约 55.09 纳秒
  • fmt.Errorf:每次操作约 210 纳秒

结论:errors.Join 性能更好,只占用 fmt.Errorf 约 25% 的时间。推荐在 Go 1.20 及以上版本使用 errors.Join 实现错误合并。

总结:

  1. Go 1.13 版本提供了易于使用的错误包装和解包函数,将错误处理变成对错误链的处理。
  2. Go 1.20 版本的 errors.Join 增强了错误合并功能,主要是性能优化。
  3. errors.Join 相比 fmt.Errorf 在实现不定长 error 合并时更加方便。

下一步:分析 errors 包的源代码实现。


013-go语言errors(中)错误链和原理分析









文章目录