Golang中的错误处理

例如在代码中,经常会看到:

1
2
3
4
xxx, err := fun() 
if err != nil {
xxx
}

自定义错误

返回的error是一个接口

1
2
3
type error interface {
Error() string
}

实现了这个接口即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type myError struct {
describe string
}

func (m myError) Error() string {
return m.describe
}

func mayError(e bool) error {
err := myError{}
if e {
err.describe = "get error"
}
return err
}

但是在验证过程中需要注意,这样返回的interface有类型,因此无论是否有报错,都无法与nil做判断。

错误接口类型断言

通过类型断言,可以判断错误是否是自定义的错误

1
2
3
4
5
6
switch err.(type) {
case myError:
fmt.Println("this is my Error")
default:
fmt.Println("unknown error")
}

error接口

内置的errors包中实现Error接口

1
2
3
4
5
6
7
8
9
10
11
func New(text string) error {      // 返回一个指针,原因是如果返回结构体,如果字符串内容一致,可能在上层判断会误判两个错误是相等的。
return &errorString{text}
}

type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type errorString string

func (e errorString)Error()string {
return string(e)
}

func NewError(text string ) error {
return errorString(text)
}

var ErrNameType = NewError("EOF")
var ErrStructType = errors.New("EOF")

func main() {
if ErrNameType == NewError("EOF") {
fmt.Println("Named Type Error") // 判断相等,这是因为直接判断了字符串是否相等
}

if ErrStructType == errors.New("EOF") { // 不想等,返回的是指针,这不是同一个变量
fmt.Println("Struct Type Error")
}
}

又或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type errorString struct {
e string
}

func (e errorString)Error()string {
return e.e
}

func NewError(text string ) error {
return errorString{e:text}
}

var ErrNameType = NewError("EOF")
var ErrStructType = errors.New("EOF")

func main() {
if ErrNameType == NewError("EOF") {
fmt.Println("Named Type Error") // 依然相等,这是因为返回结构体,结构体类型和值都是一致的。
}

if ErrStructType == errors.New("EOF") {
fmt.Println("Struct Type Error")
}
}

errors包中的方法

解包装

1
func Unwrap(err error) error

例如

1
2
3
4
err1 := errors.New("error1")
err2 := fmt.Errorf("error2: [%w]", err1) // 包装
fmt.Println(err2)
fmt.Println(errors.Unwrap(err2))

输出

1
2
error2: [error1]
error1

源码包的 src/bufio/bufio.go 中,可以看到使用errors

1
2
3
4
5
6
var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)

error的命名,建议是包名: 报错信息

判断这个是不是特定的某个error

1
func Is(err error, target error) bool

例如

1
2
fmt.Println(errors.Is(err1, err2))        // false 
fmt.Println(errors.Is(err2, err1)) // true

类型转换为特定的error

1
func As(err error, target any) bool

例如

1
2
3
4
5
6
7
8
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError // 推荐使用自定义的error type
if errors.As(err, &pathError) { // 将err转换为fs.PathError,除了as,还可以使用类型断言,
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}

输出

1
Failed at path: non-existing

fs包中的PathError

1
2
3
4
5
6
7
type PathError struct {
Op string
Path string
Err error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

第三方error包

golangerror的处理比较难以处理好,如果打印报错,为后续排查做定位,抛给上层,但是上层依然需要打印日志,此时日志打印就会出现一堆报错的情况。

另外,自定义错误在调用者判断时,由于需要判断是否一致,因此无法二次包装。

error处理遵循:

  1. handler则不往上抛,不能handler,则由上层处理,这样就可以避免函数处理错误,调用者再处理一遍
  2. 错误要被日志记录
  3. handler错误,则需要保证100%完整性

github.com/pkg/errors提供了一种错误处理方案,可以封装error,获取原生error,以及打印error的调用栈。相对于原生errors包,增加了错误堆栈信息的打印,在这方面上,建议使用pkg/errors

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"fmt"
"github.com/pkg/errors"
"io"
"log"
"os"
)

var (
NoSuchFileError = errors.New("my no such file") // 自定义错误,可以用户后续 == 判断
)

func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
return errors.Wrap(err, "write failed")
}

func WriteConfig(file string, buf []byte) error {
f, err := os.Open(file)
if err != nil {
err = errors.Wrap(NoSuchFileError, "open file failed")
return err
}
err = Write(f, buf)
return nil
}

func main() {
conf := []byte("k1 = v1")
err := WriteConfig("./config.ini", conf)
if err != nil {
log.Printf("root error: %v\n", err) // 打印原始错误
if errors.Cause(err) == NoSuchFileError {
fmt.Println("This is my error")
}
log.Printf("write config failed: %+v\n", err) // 打印错误堆栈信息
}
}

panic

error指返回一般性错误,程序可以继续正常执行,panic函数返回的让程序崩溃的错误,程序中断运行。例如数组越界、空指针引用,都会引起panic异常。panic函数接受任何参数。

内建函数

1
func panic(v any)

panic时,会将堆栈和goroutine信息输出到控制台

1
2
3
4
5
goroutine 1 [running]:
main.testA(...)
xxx/panic/main.go:6
main.main()
xxx/panic/main.go:15 +0x30

panic之后,程序崩溃退出,后续代码就不会被执行。

结合defer

defer语句在return之前,panic之后运行,可以通过recover捕获到panic的内容

内建函数:

1
func recover() any

例如:

1
2
defer fmt.Println("test a")
panic("this is a panic")

恢复机制recover

recover函数只有在defer调用函数中才有效

1
2
3
4
5
defer func() {
r := recover()
fmt.Println(r)
}()
panic("panic")

没有recover没有异常也会捕获,捕获为空,因此使用方式一般会判断捕获的信息

1
2
3
4
5
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()