• 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    问题来了,error 是什么?

    查看源码会发现,error 是一个包含 Error 方法的接口,返回的是实现了该接口的对象。

    type error interface {  
       Error() string  
    }
    
    • 1
    • 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  
    }
    
    • 1
    • 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")
    	}
    
    }
    
    • 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

    输出:

    Named Type Error
    
    • 1

    2. 错误类型

    2.1 Sentinel Error(预定义错误)

    其实就是先预定义一些可以预料中的错误,在使用过程中,通过判断 error 是属于哪一种 error 并进行对应的处理。

    举个栗子,在 io.EOF 就是一个预定义的错误,它是表示输入流中的结尾。

    var EOF = errors.New("EOF")
    
    • 1

    在从流中读取字符的时候,会通过判断 error 是否等于 io.EOF 来判定是否读完。注意这里是判断 error 的指针是否相等。

    n, err := reader.Read(p)  
    if err != nil {  
       if err == io.EOF {  
          fmt.Println("The resource is read!")  
          break  
       }  
    }
    
    • 1
    • 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)
    }
    
    • 1
    • 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
    	}
    }
    
    • 1
    • 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() }
    
    
    • 1
    • 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
    }
    
    • 1
    • 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    它不是扩展 error 更多的信息,而是扩展其方法。

    3. 优雅的处理错误

    3.1 无错误的正常流程代码

    无错误的正常流程代码,将成为一条直线,而不是缩进的代码。

    错误的写法:

    // no
    f, err := os.Open(path)
    if err == nil {
        // do stuff
    }
    
    // handle error
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    正确的写法:

    // ok
    f, err := os.Open(path)
    if err != nil {
        // handle error
    }
    
    // do stuff
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.2 减少不必要的判断

    func AuthenticateRequest(r *Request) error {
        err := authenticate(r.User)
        err != nil {
            return err
        }
        return nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    改为:

    func AuthenticateRequest(r *Request) error {
        return authenticate(r.User)
    }
    
    • 1
    • 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
    }
    
    • 1
    • 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()
    }
    
    
    
    • 1
    • 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
    }
    
    • 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

    我们创建一个 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
    }
    
    • 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

    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)
    	}
    }
    
    
    • 1
    • 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.
    
    • 1
    • 2
    • 3

    可以使用 fmt.Errorf 来对原始错误进行包装,除了原始错误信息之外,在添加额外得信息并返回。

    f, err := os.Open(path)
    	if err != nil {
    		return nil, fmt.Errorf("open file failed: %w", err)
    	}
    
    • 1
    • 2
    • 3
    • 4

    输出:

    2022/11/05 17:04:43 open file failed: open test.txt: The system cannot find the
    file specified.
    
    • 1
    • 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")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输出:

    no PathError
    
    • 1

    这里可以用 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)
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输出:

    test.txt
    
    • 1

    还可以通过 errors.UnWrap 来获取底层错误,将原始错误给解析出来。

    func main() {
    	_, err := ReadFile("test.txt")
    	err = errors.Unwrap(err)
    	fmt.Printf("ori err: %v", err)
    }
    
    • 1
    • 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)
    	}
    }
    
    • 1
    • 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.
    
    • 1

    但它还有一个更强的功能是会保存当前的堆栈信息,使用%+v 可以打印出来。

    func main() {
    	_, err := ReadFile("test.txt")
    	if err != nil {
    		fmt.Printf("stack track: \n%+v", err)
    	}
    }
    
    • 1
    • 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
    
    • 1
    • 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")
    	}
    
    • 1
    • 2
    • 3
    • 4

    输出:

    could not open file: open test.txt: The system cannot find the file specified.
    
    • 1

    还有几个常见的方法

    // 生成错误的同时带上堆栈信息
    func New(message string) error
    
    // 只附加调用堆栈信息
    func WithStack(err error) error
    
    // 获得最根本的错误原因
    func Cause(err error) error
    
    • 1
    • 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 如果调用其他包内的函数,通常简单的直接返回。
    if err != nil {
    	return err
    }
    
    • 1
    • 2
    • 3
    • 如果和其他库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。同样适用于和标准库协作的时候。
    f, err := os.Open(path)
    if err != nil {
    	return errors.Wrapf(err, "failed to open %q", path)
    
    • 1
    • 2
    • 3
    • 直接返回错误,而不是每个错误产生的地方到处打日志。

    • 在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 %+v 把堆栈详情记录。

    func main() {
    	err := app.Run()
    	if err != nil {
    		fmt.Printf("FATAL: %+V\n", err)
    		os.Exit(1)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 使用 errors.Cause 获取 root error,再进行和 sentinel error 判定。

    参考资料

    相关推荐

    go简单使用grpc

    欢迎关注,互相学习,共同进步~

    个人博客

    微信公众号:编程黑洞

  • 相关阅读:
    yolov4 预测框解码详解【附代码】
    你想学 Python 爬虫?看看这篇关于开发者工具神器的博客吧
    单链表的创建定义
    计组--中央处理器
    Javascript 编写一个简单的聊天机器人
    NTMFS4C05NT1G N-CH 30V 11.9A MOS管,PDF
    Spring之拦截器
    springboot竟然有5种默认的加载路径,你未必都知道
    216. 组合总和 III
    Activiz 9.2 for Linux Crack
  • 原文地址:https://blog.csdn.net/qq_22918243/article/details/127843401