lang

LangBook

11.错误处理

特立独行的Go有不同的错误处理方式: 可能发生错误的地方返回一个错误。

1. error的定义

还是已打开文件为例,使用标准库io的函数Open打开一个文件:

main.go
package main
 
import (
	"io"
	"os"
)
 
func main() {
	f, err := os.Open("go.mod")
	if err != nil { // err != nil 说明有错误
		panic(err) // 不想错了错了,直接panic让程序退出
	}
 
	// 打印到命令行
	io.Copy(os.Stdout, f) // Stdout标准输出,即命令行
}

输出

module github.com/aeilang/echo

go 1.23.3

这里的err是error类型,error就是一个接口,和其他语言不同,Go中错误就是一个值。

type error interface {
    Error() string
}

只要实现了这个Error() string方法就是一个错误。

2. 实现一个error

定义一个MyError结构体:

type MyError struct {
    Msg string
}

新建一个New这个结构体的函数:

func NewMyError(msg string) MyError {
	return MyError{
		Msg: msg,
	}
}

实现Error() string方法

func (e MyError) Error() string {
	return e.Msg
}

创建一个除法函数,在除数为0时它返回错误:

func Divide(a int, b int) (result int, err error) { // 使用命名返回值提高可读性
	if b == 0 {
		return 0, NewMyError("除数不能为0") //新建一个错误返回
	}
 
	return a / b, nil // 没有错误,返回nil
}

一个函数返回错误时,把错误放着最后面是一个好的习惯。

好的,来调用这个函数

func main() {
	result, err := Divide(10, 0)
	if err != nil { // err != nil 表示有错误
		panic(err) // 不想处理,直接panic,把程序终止。(不要在生产环境搞这种,哈哈。)
	}
	fmt.Println(result)
}

完整代码:

main.go
package main
 
import "fmt"
 
type MyError struct {
	Msg string
}
 
func NewMyError(msg string) MyError {
	return MyError{
		Msg: msg,
	}
}
 
func (e MyError) Error() string {
	return e.Msg
}
 
func Divide(a int, b int) (result int, err error) { // 使用命名返回值提高可读性
	if b == 0 {
		return 0, NewMyError("除数不能为0") //新建一个错误返回
	}
 
	return a / b, nil // 没有错误,返回nil
}
 
func main() {
	result, err := Divide(10, 0)
	if err != nil { // err != nil 表示有错误
		panic(err) // 不想处理,直接panic,把程序终止。(不要在生产环境搞这种,哈哈。)
	}
	fmt.Println(result)
}

输出

panic: 除数不能为0

这就是error的用法,一个函数可能发生错误,(就像打开文件,但文件不存在),就返回errorerror是一个接口,它的零值是nil, 如果err == nil就说明错误为空,就是没有错误;如果err != nil就说明错误不为空,就是有错误。设计得很巧妙对不对?

实际过程中,我们使用标准库errors来新建一个错误:

func Divide(a int, b int) (result int, err error) { 
	if b == 0 {
		return 0, err := errors.New("除数不能为0") 
	}
 
	return a / b, nil // 没有错误,返回nil
}

也可以使用fmt包来包裹错误

func Divide(a int, b int) (result int, err error) { 
	if b == 0 {
		return 0, fmt.Errorf("错误不为零,而 b = %d", b) 
	}
 
	return a / b, nil // 没有错误,返回nil
}

3. errors包小解

标准库errors包含了很多有用的处理错误的函数,我们已经接触了New函数用来新建一个错误

err := errors.New("...出错")

设想一下这样一个错误的链路:

fooA函数返回一个错误

func fooA() error {
	return errors.New("fooA出错")
}

fooB函数调用fooA返回一个错误

func fooB() error {
	err := fooA()
	return err
}

fooC函数调用fooB返回一个错误

func fooC() error {
	err := fooB()
	return err
}

我们在main函数中调用fooC:

main.go
func main() {
	err := fooC()
	fmt.Println(err)
}

运行,输出

fooA出错

我们得到了底层的错误fooA出错,可是并不知道错误的链路。这时可以使用fmt.Errorf将错误包裹起来

main.go
package main
 
import (
	"errors"
	"fmt"
)
 
func fooA() error {
	return errors.New("FooA出错")
}
 
func fooB() error {
	err := fooA()
	return fmt.Errorf("fooB: %v", err)  
}
 
func fooC() error {
	err := fooB()
	return fmt.Errorf("fooC: %v", err)  
}
 
func main() {
	err := fooC()
	fmt.Println(err)
}

运行,输出

fooC: fooB: FooA出错

好的,错误的链路清晰了。

有时,我们需要判断一个错误是不是特定的错误:

新建一个特定错误, fooA返回这个错误

var MyErr = errors.New("特定错误")
 
func fooA() error {
	return MyErr
}

如果还采用上述的包裹方式,就无法判断原来的错误是不是特定错误,因为fmt.Errorf()返回的是一个新的错误,只是把原来的错误信息格式化成字符串加进去了。

例如:

main.go
package main
 
import (
	"errors"
	"fmt"
)
 
var MyErr = errors.New("特定错误") 
 
func fooA() error {
	return MyErr 
}
 
func fooB() error {
	err := fooA()
	return fmt.Errorf("fooB: %v", err)
}
 
func fooC() error {
	err := fooB()
	return fmt.Errorf("fooC: %v", err)
}
 
func main() {
	err := fooC()
	fmt.Println(err)
	fmt.Println(err == MyErr) 
}

输出

fooC: fooB: 特定错误
false

输出为false, 因为这两个是不同的错误。

fmt.Errorf函数中使用%w动词进行包裹,这样原来的错误会保留在包裹的错误结构体中。

例如:

func fooB() error {
	err := fooA()
	return fmt.Errorf("fooB: %w", err) 
}

使用errors.Is(err, target)判断err中是否包裹有特定错误target

func main() {
	err := fooC()
	fmt.Println(err)
	fmt.Println(errors.Is(err, MyErr))  
}

完整代码:

main.go
package main
 
import (
	"errors"
	"fmt"
)
 
var MyErr = errors.New("特定错误")
 
func fooA() error {
	return MyErr
}
 
func fooB() error {
	err := fooA()
	return fmt.Errorf("fooB: %w", err)  
}
 
func fooC() error {
	err := fooB()
	return fmt.Errorf("fooC: %w", err)  
}
 
func main() {
	err := fooC()
	fmt.Println(err)
	fmt.Println(errors.Is(err, MyErr))  
}

运行,输出

fooC: fooB: 特定错误
true

结果是true

errors.Is(err, target), 接收一个要判断的err, 和一个目标的错误,返回目标的错误是不是包裹在err中,如果是则返回true

这样我们就能打印错误的链路和判断是否包含特定的错误了。这些知识已经基本足够,更多的错误处理在另一本书《Go标准库详解》

On this page