go中如何处理error
# 0. 前言
go 中的异常处理和其他语言大不相同,像 Java、C++、python 等语言都是通过抛出 Exception 来处理异常,而 go 是通过返回 error 来判定异常,并进行处理。
在 go 中有 panic 的机制,但 panic 意味着程序终止,代码不能继续运行了,不能期望调用者来解决它。而 error 是预期中的异常,希望调用者可以对其进行处理的。
# 1. error 是什么?
举个例子,使用 Open 来打开文件,但是可能该路径的文件不存在,出现异常,在 go 是通过判断 err 是否为 nil 来判定打开文件是否成功。
f, err := os.Open(path)
if err != nil {
// handle error
}
// do stuff
2
3
4
5
6
问题来了,error 是什么?
查看源码会发现,error 是一个包含 Error 方法的接口,返回的是实现了该接口的对象。
type error interface {
Error() string
}
2
3
我们一般使用是通过 errors.New()来返回一个实现了 error 接口的对象。这个对象是一个包含了字符串的结构体,然后可以通过 Error 方法来获取字符串。
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
2
3
4
5
6
7
8
9
10
11
12
我们可以注意到 New 方法返回的是 errorString 的地址,也就是说,在我们将两个 error 比较相等时,比较是地址,是两个 error 是否为同一个对象,而不是其中的错误字符串。
import (
"errors"
"fmt"
)
type errorString string
func (e errorString) Error() string {
return string(e)
}
func New(text string) error {
return errorString(text)
}
var ErrNamedType = New("EOF")
var ErrStructType = errors.New("EOF")
func main() {
if ErrNamedType == New("EOF") {
fmt.Println("Named Type Error")
}
if ErrStructType == errors.New("EOF") {
fmt.Println("Struct Type Error")
}
}
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
输出:
Named Type Error
# 2. 错误类型
# 2.1 Sentinel Error(预定义错误)
其实就是先预定义一些可以预料中的错误,在使用过程中,通过判断 error 是属于哪一种 error 并进行对应的处理。
举个栗子,在 io.EOF 就是一个预定义的错误,它是表示输入流中的结尾。
var EOF = errors.New("EOF")
在从流中读取字符的时候,会通过判断 error 是否等于 io.EOF 来判定是否读完。注意这里是判断 error 的指针是否相等。
n, err := reader.Read(p)
if err != nil {
if err == io.EOF {
fmt.Println("The resource is read!")
break
}
}
2
3
4
5
6
7
这种方式不建议使用,原因是:
- 它会成为你 API 的公共部分
因为公共函数需要返回一个固定的 error,那么这个 error 就必须是公开的,那么就需要文档记录,这会增加 API 的表面积。
- 增加调用者的耦合性
调用者必须要知道 io.EOF 这个 error ,并在调用的地方使用该 error 判断是否结束。
# 2.2 Error types(自定义错类型)
通过实现 error 接口来创建自定义错误类型。和 Sentinel Error 相比,是通过判断类型来知道是哪种错误,并且可以输出更多的上下文错误信息。
通过自定义 MyError,并实现 error 接口中的 Error 的方法。
type MyError struct {
Msg string
File string
Line int
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}
2
3
4
5
6
7
8
9
test 方法中返回的是自定义的 error,我们通过断言转换 error 成 MyError 类型,然后再输出更多的上下文信息。
func test() error {
return &MyError{"Something happened", "server.go", 42}
}
func main() {
err := test()
switch err := err.(type) {
case nil:
// success
case *MyError:
fmt.Println("error occured on line:", err.Line)
default:
// unknown error
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在标准库 os.PathError
中,自定义了 PathError ,也是相同的用法。
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
2
3
4
5
6
7
8
我们也尽可能避免使用 Error types,因为它和 Sentinel Erorr 一样会和调用者产生耦合,会作为 API 的一部分。
# 2.3 Opaque errors(不透明的错误)
Error types 是通过判断 error 的类型来走不同的逻辑,而 Opaque errors 是通过判断 error 的行为来走不同的逻辑。
在 net.Error 中定义如下,除了包含 error 外,还包含 Timeout 和 Temporary 方法。
type Error interface {
error
Timeout() bool
Temporary() bool
}
2
3
4
5
除了判断是否有 error 之外,还可以通过方法来判断是哪种类型的 error 然后进行对应的处理。
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return false
}
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
return false
}
2
3
4
5
6
7
它不是扩展 error 更多的信息,而是扩展其方法。
# 3. 优雅的处理错误
# 3.1 无错误的正常流程代码
无错误的正常流程代码,将成为一条直线,而不是缩进的代码。
错误的写法:
// no
f, err := os.Open(path)
if err == nil {
// do stuff
}
// handle error
2
3
4
5
6
7
正确的写法:
// ok
f, err := os.Open(path)
if err != nil {
// handle error
}
// do stuff
2
3
4
5
6
7
8
# 3.2 减少不必要的判断
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
err != nil {
return err
}
return nil
}
2
3
4
5
6
7
改为:
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
2
3
# 3.3 将 error 内部存储起来
err 在内部临时储存,在最后在返回出来。
下面例子中,通过循环读 reader 每一行的数据,每次判断 err 是不是 nil 来判断是否读完,如果是则退出循环,再返回。
func CountLines(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
if err != io.EOF {
return 0, err
}
return lines, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
改进版本:
type Scanner struct {
err error // Sticky error.
...
}
func CountLines(r io.Reader) (int, error) {
sc := bufio.NewScanner(r)
lines := 0
for sc.Scan() {
lines++
}
return lines, sc.Err()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
每次循环都会判断 Scan 的返回值,当无内容返回时,则会返回 False,则结束循环,并返回结果。循环中出现的 error 会在 Scan 中通过 s.setErr(err) 保存在对象的 err 属性中。
代码明显简洁了许多。
# 3.4 将重复操作抽离出来
看看下面的代码,里面多次使用 fmt.Fprintf()并判断其返回值是否为 err
type Header struct {
Key, Value string
}
type Status struct {
Code int
Reason string
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}
for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
return err
}
_, err = io.Copy(w, body)
return err
}
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
我们创建一个 errWrite 结构体并实现 Write 方法,也就是在原来的 write 方法中包了一层并做好错误判断,然后在每一个 fmt.Fprintf 使用我们定义的 errWrite 进行写入,这样就达到了复用的效果,代码也好看了许多。
type errWrite struct {
io.Writer
err error
}
func (e *errWrite) Write(buf []byte) (int, error) {
if e.err != nil {
return 0, e.err
}
var n int
n, e.err = e.Writer.Write(buf)
return n, e.err
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew := &errWrite{Writer: w}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
for _, h := range headers {
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}
fmt.Fprintf(w, "\r\n")
io.Copy(ew, body)
return ew.err
}
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
# 4. Wrap erros
在我们开发中,常常会在错误处理中,记录了日志,并且将错误给返回了。
在 os.Open
找不到文件时会返回 error,处理 error 时,将 error 的信息打上日志,并且将 err 进行返回,在 main 函数中,拿到 error 后再次打上 error 的日志,这个日志和上面有部分是重复的日志。
在代码调用链多的时候,会打上更多的重复日志,日志中出现非常多的噪音,非常影响排查错误。
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
log.Printf("could not open file: %v", err)
return nil, err
}
defer f.Close()
read := bufio.NewReader(f)
line, _, err := read.ReadLine()
return line, err
}
func main() {
_, err := ReadFile("test.txt")
if err != nil {
fmt.Println(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行输出:
2022/11/05 17:03:16 could not open file: open test.txt: The system cannot find t
he file specified.
2022/11/05 17:03:16 open test.txt: The system cannot find the file specified.
2
3
可以使用 fmt.Errorf
来对原始错误进行包装,除了原始错误信息之外,在添加额外得信息并返回。
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file failed: %w", err)
}
2
3
4
输出:
2022/11/05 17:04:43 open file failed: open test.txt: The system cannot find the
file specified.
2
fmt.Errorf 返回的是一个新的被包装的 error,errors.Is
可以一层一层的剥开包装来判断是否为原始错误,但是它是做指针判断的,这里 os.Open 返回的原始错误是 os.PathError
但是因为返回的是地址,所以无法用 errors.Is 判断。
func main() {
_, err := ReadFile("test.txt")
var pathError *os.PathError
if errors.Is(err, pathError) {
fmt.Println("is PathError")
} else {
fmt.Println("no PathError")
}
}
2
3
4
5
6
7
8
9
10
11
输出:
no PathError
这里可以用 errors.As
来判断 err 是否为 os.PathError
类型,即使 err 是地址。
这里判断了是否为 os.PathError
错误,并且将返回的 err 转换成了该错误,我们可以调用其中的属性来获取更多的信息。
func main() {
_, err := ReadFile("test.txt")
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Println(pathError.Path)
}
}
2
3
4
5
6
7
8
9
10
11
输出:
test.txt
还可以通过 errors.UnWrap
来获取底层错误,将原始错误给解析出来。
func main() {
_, err := ReadFile("test.txt")
err = errors.Unwrap(err)
fmt.Printf("ori err: %v", err)
}
2
3
4
5
# 5. pkg/errors
上面介绍的都是原生的 errors 处理模块,现在介绍 pkg/errors 模块,完全兼容原生 errors,并且对其进行增强,主要是添加了保存堆栈的能力。
可以使用 errors.Wrap 进行对 error 的包装,并添加额外的信息。
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "could not open file")
}
defer f.Close()
read := bufio.NewReader(f)
line, _, err := read.ReadLine()
return line, err
}
func main() {
_, err := ReadFile("test.txt")
if err != nil {
fmt.Println(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
输出:
could not open file: open test.txt: The system cannot find the file specified.
但它还有一个更强的功能是会保存当前的堆栈信息,使用%+v 可以打印出来。
func main() {
_, err := ReadFile("test.txt")
if err != nil {
fmt.Printf("stack track: \n%+v", err)
}
}
2
3
4
5
6
输出:
stack track:
open test.txt: The system cannot find the file specified.
could not open file
main.ReadFile
D:/code/go_demo/main3.go:13
main.main
D:/code/go_demo/main3.go:24
runtime.main
D:/install/go18.3/src/runtime/proc.go:250
runtime.goexit
D:/install/go18.3/src/runtime/asm_amd64.s:1571
2
3
4
5
6
7
8
9
10
11
如果不想保存堆栈信息,只添加额外的信息,可以使用 errors.WithMessage
添加。
f, err := os.Open(path)
if err != nil {
return nil, errors.WithMessage(err, "could not open file")
}
2
3
4
输出:
could not open file: open test.txt: The system cannot find the file specified.
还有几个常见的方法
// 生成错误的同时带上堆栈信息
func New(message string) error
// 只附加调用堆栈信息
func WithStack(err error) error
// 获得最根本的错误原因
func Cause(err error) error
2
3
4
5
6
7
8
# 6. error 的最佳实践
处理 error 的方式这么多,我们该如何最优的使用它们呢?有以下几个方法:
- 在自己的应用代码中,使用 errors.New 或者 errors.Errorf 来返回错误
func parseArgs(args []string) error {
if len(args) < 3 {
return errors.Errorf("not enough arguments")
}
return nil
}
2
3
4
5
6
7
- 如果调用其他包内的函数,通常简单的直接返回。
if err != nil {
return err
}
2
3
- 如果和其他库进行协作,考虑使用
errors.Wrap
或者errors.Wrapf
保存堆栈信息。同样适用于和标准库协作的时候。
f, err := os.Open(path)
if err != nil {
return errors.Wrapf(err, "failed to open %q", path)
2
3
直接返回错误,而不是每个错误产生的地方到处打日志。
在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 %+v 把堆栈详情记录。
func main() {
err := app.Run()
if err != nil {
fmt.Printf("FATAL: %+V\n", err)
os.Exit(1)
}
}
2
3
4
5
6
7
8
- 使用 errors.Cause 获取 root error,再进行和 sentinel error 判定。