原文链接:https://xiets.blog.csdn.net/article/details/130864936
版权声明:原创文章禁止转载
专栏目录:Golang 专栏(总目录)
Go 调用可能发生异常的函数时,通过多返回一个附加结果作为错误值,习惯上将错误值作为最后一个返回结果。如果返回的错误值为错误类型的零值(如 nil
),表示没有发生错误。
1. error 接口
Go 的内置类型中定义了一个 error 接口,用于表示一个错误的通用类型。error 接口的原型:
type error interface {
Error() string
}
error 接口有一个方法 Error() string
,用于返回错误的描述信息。需要错误处理的函数的返回值将包含一个 error 对象,如果 error 是 nil
表示成功(无错误),非 nil
表示失败(有错误)。
2. 只有一种错误的情况
如果错误只有一种情况,一般使用 bool 类型作为错误值类型。例如查询一个 key 在 map 中是否存在:
map1 := make(map[string]string)
...
value, ok := map1[key]
if !ok {
// map1[key] 不存在
}
3. 有多种错误的情况
更多时候,尤其是 I/O 操作,错误原因可能多种多样,需要给调用者返回一些详细的错误信息。
一般当函数返回一个非 nil
的错误时,它的其他返回值是不明确的而且应该忽略。但有一些函数在出错的情况下可能会返回部分有用的结果(具体看函数的注释文档),例如读取一个文件时发生错误,Read() 函数返回能够成功读取的字节数和对应的错误值。
错误处理示例:
package main
import (
"bytes"
"fmt"
"io"
"os"
)
func main() {
// 打开文件
f, err := os.Open("demo.txt")
// 处理打开时的错误
if err != nil {
// 可以判断错误的类型
if os.IsNotExist(err) {
fmt.Printf("文件不能存在: %v\n", err)
} else {
fmt.Printf("其他错误: %v\n", err)
}
return
}
// 函数结束时关闭文件
defer f.Close()
// 手动读取文件
buf := bytes.NewBuffer(nil)
bs := make([]byte, 32)
for {
n, err := f.Read(bs)
if err != nil {
if err == io.EOF {
fmt.Printf("到达文件末尾: %v\n", err)
} else {
fmt.Printf("其他错误: %v\n", err)
}
if n > 0 {
// 可能部分成功的情况
buf.Write(bs[:n])
}
break
}
buf.Write(bs[:n])
}
// 输出读取到的文件内容
fmt.Printf("文件内容: %s\n", buf.String())
}
使用 “%v” 或 Print 函数输出 error 类型的变量,将调用
Error() string
方法的返回值作为默认的字符串输出格式。
4. 创建错误: errors.New() 和 fmt.Errorf()
在自定义的函数中,如果需要返回一个错误类型,可以自己创建一个错误类型,并实现 error
接口。也可以调用 errors.New()
或 fmt.Errorf()
函数创建一个实现了 error
接口的简单的错误对象,前者传递一个字符串作为错误信息,后者则是格式化一个字符串作为错误信息。
函数原型:
errors.New(text string) error
fmt.Errorf(format string, a ...interface{}) error
代码实例:
package main
import (
"fmt"
)
func main() {
c, err := division(2, 0)
if err != nil {
fmt.Printf("发生错误: %v\n", err) // 发生错误: 错误: 2 / 0, 除数不能为0
return
}
fmt.Println(c)
}
func division(a, b int) (int, error) {
if b == 0 {
err := fmt.Errorf("错误: %d / %d, 除数不能为0", a, b)
// err := errors.New("除数不能为0")
return 0, err
}
c := a / b
return c, nil
}
5. 错误处理机制
在自定义的函数中调用函数发生错误,一般需要根据错误的类型进行相应的处理,如果无法处理,可以把错误传递下去(返回给上级调用者):
func ReadFile(file string) ([]byte, error) {
f, err := os.Open(file)
if err != nil {
// 返回错误信息给上级调用者
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
自定义函数中调用函数返回错误时,如果需要把更多信息返回给上级调用者,可以创建一个新的错误对象返回:
func ReadFile(file string) ([]byte, error) {
f, err := os.Open(file)
if err != nil {
// 创建一个新的错误对象返回, 同时必须附加上原来的错误信息
err := fmt.Errorf("open file(%s) error: %v", file, err)
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
使用 fmt.Errorf()
函数格式化一条错误信息,并返回新的错误对象。在每个函数内传递错误时,为原始的错误信息不断地添加额外的上下文信息,以此建立一个可读可追溯的错误描述。当错误最终被 main 函数处理时,它就得到了一个从最根本问题到总体问题的清晰因果链。
因为错误信息被频繁地使用 err = fmt.Errorf("call func error desc: %v", err)
串联起来,所以错误信息描述字符串首字母不应该大写并且避免换行。一般地,调用函数 f(x) 发生错误,只需要报告 函数f的行为 和 参数值x,因为它们直接和错误上下文相关。
6. 基础 error
上面介绍 error 是属于 Go 1.13 之前的错误处理方式。Go 1.13 之前,标准库对 error 只做了简单的支持,只有 errors.New()
和 fmt.Errorf()
两种方式构造 error 实例,而这两种方式构造出来的 error 都是只对 error 做了简易实现的 errors.errorString
类型。errors
包中的简易 error 实现:
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
如果在一个函数中,可能返回多种类型错误,如果使用基础 error 实现,一般有两种方式:
- 事先创建全局 error 实例,比如创建一个
NotExistError
全局变量,函数中遇到“不存在”的错误,就返回这个实例,然后上层调用者接收到 error 后再通过==
与NotExistError
全局变量比较来判断是否是“不存在”的错误。这种方式虽然可以区分不同的错误类型,但无法携带上下文信息。 - 自定义错误类型实现 error 接口,调用者接收到一个 error 后,通过类型断言的方式判断是否是自定义的错误类型。
上面两种方式,在函数需要多级调用时,如果在每一个函数中都需要携带上下文信息,使用 err = fmt.Errorf("xxx: %v", err)
串联 error,那么最终也会丢失掉原始的 error 对象,仅仅传递了错误的文本描述而已。
7. 链式/树状 error
7.1 链式 wrapError
为了解决原始错误对象丢失问题,Go 1.13 在保持兼容原有 error 机制的前提下,在 fmt
包中提供了新的 wrapError
类型:
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
wrapError
是结构体类型,并且实现了 error 接口。结构体中使用 msg 字段来存储当前错误的描述信息,并且还增加一个字段 err 用于存储原始的 error 对象,通过 Unwrap()
方法可以获取出上一个错误对象。
wrapError
实例需要通过 fmt.Errorf()
格式化的方式创建,Go 1.13 新增加了 %w
格式化动词用于匹配 error 对象。如果 fmt.Errorf()
存在 %w
,则内部创建 wrapError 对象,并把 %w
匹配到的 error 对象存储到 wrapError.err 属性中,最终返回 wrapError 对象。如果没有 %w
,则 fmt.Errorf()
内部还是走原始的 errors.New()
的方式返回 errorString
对象。
wrapError
像链表一样,一个 error 链接了另一个 error,因此也被称为 链式error。
%w
只能用在fmt.Errorf()
函数,并且只能匹配error
类型。
7.2 树状 wrapErrors
Go 1.13 的 wrapError
内部只能存储一个原始 error,因此一次 fmt.Errorf()
只能 wrap 一个 error,也就是 format 中只能出现一个 %w
。当一个函数中如果有多个错误需要同时被 wrap,就需要多次 fmt.Errorf()
。Go 1.20 在保持兼容原有 wrapError 机制的前提下,在 fmt
包中又提供了新的 wrapErrors
类型:
type wrapErrors struct {
msg string
errs []error
}
func (e *wrapErrors) Error() string {
return e.msg
}
func (e *wrapErrors) Unwrap() []error {
return e.errs
}
当使用 fmt.Errorf()
串联 error 时,如果 format 中没有 %w
,则走传统的 errors.New()
;如果只有 1 个 %w
,则封装为 wrapError
;如果 %w
的数量大于等于 2,则封装为 wrapErrors
,把多个 %w
匹配到的 error 存放到 wrapErrors.errs
切片属性中。
wrapErrors
类型的一个 error 可以包含多个 error,其中包含的 error 如果也有 wrapErrors
,就形成了树状结构,因此 wrapErrors
也被称为 树状error。
7.3 errors 包
errors
包中封装了许多操作 wrapError
和 wrapErrors
的函数:
// 把文本封装为 errorString 类型的简单错误
errors.New(text string) error
// 判断 err 错误对象中是否包含了 target 错误对象 (通过 == 判断对象, 不是判断类型), 兼容以前任意方式实现的 error。
//
// 内部通过递归判断:
// 如果 err 没有 Unwrap() 方法 (传统的 errorString), 则直接通过 err == target 判断
// 如果 err 有 Unwrap() error 方法的 (wrapError), 则通过解包再递归调用判断, 相当于 Is(err.Unwrap(), target)
// 如果 err 有 Unwrap() []error 方法的 (wrapErrors), 则遍历 []error 中的每一个 error, 再递归调用 Is(error, target) 去判断
// 也就是说, 只要 error 是通过 fmt.Errorf() 包装的, 通过 Is() 方法就能判断出最终包装出来的 err 是否包含原 error。
//
// 判断 err 是否是某个错误, 应该使用 errors.Is() 才能兼容新旧类型的 error 机制, 而不是 ==
//
errors.Is(err, target error) bool
// 把多个 err 封装为 errors.joinError 类型的 error, 相对于 wrapErrors 类型但没有 msg 字段
errors.Join(errs ...error) error
// 如果 err 有 Unwrap() error 方法, 则调用 Unwrap() 并返回返回值, 其他情况均返回 nil。如果是 err 是 wrapErrors 也返回 nil。
errors.Unwrap(err error) error
// 在 err 的树状结构中找到第一个与 target 数据类型匹配的 error, 如果找到, 则将 error 存储到 target 并返回 true。否则, 它返回 false。
// 该函数主要用于在一个多层链式/树状包裹的 error 中, 提取想要的目标类型的 error 对象。内部也是使用不断递归 Unwrap() 去寻找目标类型的 error。
errors.As(err error, target any) bool
7.4 error 代码示例
代码示例一:判断返回的 error 是否包含指定的错误
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
)
// DirNotExists 定义错误类型的实例
var DirNotExists = errors.New("dir not exists")
func main() {
err := SaveFile("hello.txt", "hello world")
if err != nil {
fmt.Println("err:", err)
if errors.Is(err, DirNotExists) {
fmt.Println("是缓存文件夹不存在导致的错误")
}
}
}
func SaveFile(name string, content string) error {
cacheDir, err := GetCacheDir()
if err != nil {
// %w包装原错误, 并携带上当前函数中的上下文信息
err = fmt.Errorf("name=%s: %w", name, err)
return err
}
err = os.WriteFile(filepath.Join(cacheDir, name), []byte(content), 0666)
return err
}
func GetCacheDir() (string, error) {
// 演示获取缓存目录失败, 直接返回错误
return "", DirNotExists
}
// Output:
// err: name=hello.txt: dir not exists
// 是缓存文件夹不存在导致的错误
代码示例二:从包装的 error 中获取出指定类型的错误对象
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
)
// DirNotExists 定义错误类型
type DirNotExists struct {
DirPath string
}
func (err DirNotExists) Error() string {
return "file not exists: " + err.DirPath
}
func main() {
err := SaveFile("hello.txt", "hello world")
if err != nil {
fmt.Println("err:", err)
dirNotExists := &DirNotExists{}
if ok := errors.As(err, dirNotExists); ok {
fmt.Println("缓存文件夹路径:", dirNotExists.DirPath)
}
}
}
func SaveFile(name string, content string) error {
cacheDir, err := GetCacheDir()
if err != nil {
// %w包装原错误, 并携带上当前函数中的上下文信息
err = fmt.Errorf("name=%s: %w", name, err)
return err
}
err = os.WriteFile(filepath.Join(cacheDir, name), []byte(content), 0666)
return err
}
func GetCacheDir() (string, error) {
cacheDir := "./cache_dir"
// 演示获取缓存目录失败, 直接返回错误
err := DirNotExists{
DirPath: cacheDir,
}
return "", err
}
// Output:
// err: name=hello.txt: file not exists: ./cache_dir
// 缓存文件夹路径: ./cache_dir
8. 宕机(panic) 与 恢复(recover)
Go 没有类似其他语言的 try catch
机制捕获错误。Go 可以通过 defer 语句添加延迟函数来捕获宕机错误。
当 Go 程序发生宕机时,正常的程序执行将被终止,已经通过 defer 添加的所有延迟函数将会执行。如果没有在延迟函数中捕获错误恢复程序,则所有延迟函数执行完后将退出进程。
程序中也可以手动调用内置函数 panic()
发生宕机,该函数可以接收任何值作为参数(通常是 error 类型的值)。发生宕机后,后面的程序代码将不再执行。在延迟函数中可以调用 recover()
方法获取 painc() 发布的宕机错误值。当一个宕机错误被延迟函数 recover() 接收返回后,程序将被恢复(进程不会退出),函数将正常返回。当宕机被恢复后,函数返回值如果没有明确赋值,则返回其类型的零值。如果一个函数中发生宕机错误没有被捕获(程序没有被恢复),将把宕机错误传递给上级调用者,直到传递到 main() 函数中仍然没有捕获宕机错误,程序进程将退出。
如果没有发生宕机错误 或 已有的宕机错误已被捕获,此时调用 recover()
方法将返回 nil
。
宕机示例:
package main
import (
"fmt"
)
func main() {
fmt.Println("main() start")
// 如果 hello() 函数发生了宕机且程序没有恢复, 则宕机错误将传递到当前函数中,
// 相当于此处发生了宕机, 后面的代码将不再执行。如果在当前函数中继续不捕获宕机错误恢复程序,
// 将继续抛给上级调用者, 直到在 main() 函数中依然没有被捕获, 进程将(异常)退出。
hello()
fmt.Println("main() end")
}
func hello() {
fmt.Println("hello() start")
a := 0
a = 1 / a // 除数不能为0, 这里将发生宕机, 后面的语句将不再执行, 然后执行延迟函数
fmt.Println("hello() end")
}
// 结果输出:
// main() start
// hello() start
宕机与恢复示例:
package main
import (
"fmt"
)
func main() {
fmt.Println("main() start")
hello()
fmt.Println("main() end")
}
func hello() {
fmt.Println("hello() start")
defer func() {
err := recover() // 捕获 hello() 函数中发生的宕机错误, 恢复程序
fmt.Printf("call hello() err: %v\n", err)
// 宕机错误已被捕获, 程序已被恢复, 对于调用者来说 hello() 函数将正常返回
}()
a := 0
a = 1 / a // 除数不能为0, 这里将发生宕机, 后面的语句将不再执行, 然后执行延迟函数
fmt.Println("hello() end")
}
// 结果输出:
// main() start
// hello() start
// call hello() err: runtime error: integer divide by zero
// main() end
手动发生宕机:
package main
import (
"fmt"
)
func main() {
fmt.Println("main() start")
hello()
fmt.Println("main() end")
}
func hello() {
defer func() {
err := recover() // 捕获宕机错误, 恢复程序
fmt.Printf("error: %v\n", err)
}()
c, r := division(5, 0)
fmt.Println(c, r)
}
// division a 除以 b, 返回 商 和 余数
func division(a, b int) (int, int) {
if b == 0 {
err := fmt.Errorf("%d / %d 错误, 除数不能为0", a, b)
panic(err) // 发生宕机, 手动抛出宕机错误给上级调用者, 后面的代码将不再执行
}
c := a / b
r := a % b
return c, r
}
// 结果输出:
// main() start
// error: 5 / 0 错误, 除数不能为0
// main() end