特立独行的Go有不同的错误处理方式: 可能发生错误的地方返回一个错误。
还是已打开文件为例,使用标准库io
的函数Open
打开一个文件:
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
方法就是一个错误。
定义一个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)
}
完整代码:
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)
}
输出
这就是error的用法,一个函数可能发生错误,(就像打开文件,但文件不存在),就返回error
。 error
是一个接口,它的零值是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
}
标准库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
:
func main() {
err := fooC()
fmt.Println(err)
}
运行,输出
我们得到了底层的错误fooA
出错,可是并不知道错误的链路。这时可以使用fmt.Errorf
将错误包裹起来
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)
}
运行,输出
好的,错误的链路清晰了。
有时,我们需要判断一个错误是不是特定的错误:
新建一个特定错误, fooA
返回这个错误
var MyErr = errors.New("特定错误")
func fooA() error {
return MyErr
}
如果还采用上述的包裹方式,就无法判断原来的错误是不是特定错误,因为fmt.Errorf()
返回的是一个新的错误,只是把原来的错误信息格式化成字符串加进去了。
例如:
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)
}
输出
输出为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))
}
完整代码:
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))
}
运行,输出
结果是true
。
errors.Is(err, target), 接收一个要判断的err, 和一个目标的错误,返回目标的错误是不是包裹在err中,如果是则返回true
。
这样我们就能打印错误的链路和判断是否包含特定的错误了。这些知识已经基本足够,更多的错误处理在另一本书《Go标准库详解》