go中如何处理error

2023-03-06 18:58:31 浏览数 (2)

# 0. 前言

go 中的异常处理和其他语言大不相同,像 Java、C 、python 等语言都是通过抛出 Exception 来处理异常,而 go 是通过返回 error 来判定异常,并进行处理。

在 go 中有 panic 的机制,但 panic 意味着程序终止,代码不能继续运行了,不能期望调用者来解决它。而 error 是预期中的异常,希望调用者可以对其进行处理的。

# 1. error 是什么?

举个例子,使用 Open 来打开文件,但是可能该路径的文件不存在,出现异常,在 go 是通过判断 err 是否为 nil 来判定打开文件是否成功。

代码语言:javascript复制
f, err := os.Open(path)
if err != nil {
    // handle error
}

// do stuff

问题来了,error 是什么?

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

代码语言:javascript复制
type error interface {  
   Error() string  
}

我们一般使用是通过 errors.New()来返回一个实现了 error 接口的对象。这个对象是一个包含了字符串的结构体,然后可以通过 Error 方法来获取字符串。

代码语言:javascript复制
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  
}

我们可以注意到 New 方法返回的是 errorString 的地址,也就是说,在我们将两个 error 比较相等时,比较是地址,是两个 error 是否为同一个对象,而不是其中的错误字符串。

代码语言:javascript复制
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")
	}

}

输出:

代码语言:javascript复制
Named Type Error

# 2. 错误类型

# 2.1 Sentinel Error(预定义错误)

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

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

代码语言:javascript复制
var EOF = errors.New("EOF")

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

代码语言:javascript复制
n, err := reader.Read(p)  
if err != nil {  
   if err == io.EOF {  
      fmt.Println("The resource is read!")  
      break  
   }  
}

这种方式不建议使用,原因是:

  • 它会成为你 API 的公共部分

因为公共函数需要返回一个固定的 error,那么这个 error 就必须是公开的,那么就需要文档记录,这会增加 API 的表面积。

  • 增加调用者的耦合性

调用者必须要知道 io.EOF 这个 error ,并在调用的地方使用该 error 判断是否结束。

# 2.2 Error types(自定义错类型)

通过实现 error 接口来创建自定义错误类型。和 Sentinel Error 相比,是通过判断类型来知道是哪种错误,并且可以输出更多的上下文错误信息。

通过自定义 MyError,并实现 error 接口中的 Error 的方法。

代码语言:javascript复制
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)
}

test 方法中返回的是自定义的 error,我们通过断言转换 error 成 MyError 类型,然后再输出更多的上下文信息。

代码语言:javascript复制
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
	}
}

在标准库 os.PathError 中,自定义了 PathError ,也是相同的用法。

代码语言:javascript复制
type PathError struct {
	Op   string
	Path string
	Err  error
}

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

我们也尽可能避免使用 Error types,因为它和 Sentinel Erorr 一样会和调用者产生耦合,会作为 API 的一部分。

# 2.3 Opaque errors(不透明的错误)

Error types 是通过判断 error 的类型来走不同的逻辑,而 Opaque errors 是通过判断 error 的行为来走不同的逻辑。

在 net.Error 中定义如下,除了包含 error 外,还包含 Timeout 和 Temporary 方法。

代码语言:javascript复制
type Error interface {
	error
	Timeout() bool 
	Temporary() bool
}

除了判断是否有 error 之外,还可以通过方法来判断是哪种类型的 error 然后进行对应的处理。

代码语言:javascript复制
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
	return false
}

if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
	return false
}

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

# 3. 优雅的处理错误

# 3.1 无错误的正常流程代码

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

错误的写法:

代码语言:javascript复制
// no
f, err := os.Open(path)
if err == nil {
    // do stuff
}

// handle error

正确的写法:

代码语言:javascript复制
// ok
f, err := os.Open(path)
if err != nil {
    // handle error
}

// do stuff

# 3.2 减少不必要的判断

代码语言:javascript复制
func AuthenticateRequest(r *Request) error {
    err := authenticate(r.User)
    err != nil {
        return err
    }
    return nil
}

改为:

代码语言:javascript复制
func AuthenticateRequest(r *Request) error {
    return authenticate(r.User)
}

# 3.3 将 error 内部存储起来

err 在内部临时储存,在最后在返回出来。

下面例子中,通过循环读 reader 每一行的数据,每次判断 err 是不是 nil 来判断是否读完,如果是则退出循环,再返回。

代码语言:javascript复制
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
}

改进版本:

代码语言:javascript复制
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()
}

每次循环都会判断 Scan 的返回值,当无内容返回时,则会返回 False,则结束循环,并返回结果。循环中出现的 error 会在 Scan 中通过 s.setErr(err) 保存在对象的 err 属性中。

代码明显简洁了许多。

# 3.4 将重复操作抽离出来

看看下面的代码,里面多次使用 fmt.Fprintf()并判断其返回值是否为 err

代码语言:javascript复制
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 %srn", st.Code, st.Reason)
	if err != nil {
		return err
	}

	for _, h := range headers {
		_, err := fmt.Fprintf(w, "%s: %srn", h.Key, h.Value)
		if err != nil {
			return err
		}
	}

	if _, err := fmt.Fprintf(w, "rn"); err != nil {
		return err
	}

	_, err = io.Copy(w, body)
	return err
}

我们创建一个 errWrite 结构体并实现 Write 方法,也就是在原来的 write 方法中包了一层并做好错误判断,然后在每一个 fmt.Fprintf 使用我们定义的 errWrite 进行写入,这样就达到了复用的效果,代码也好看了许多。

代码语言:javascript复制
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 %srn", st.Code, st.Reason)

	for _, h := range headers {
		fmt.Fprintf(ew, "%s: %srn", h.Key, h.Value)
	}

	fmt.Fprintf(w, "rn")

	io.Copy(ew, body)
	return ew.err
}

# 4. Wrap erros

在我们开发中,常常会在错误处理中,记录了日志,并且将错误给返回了。

os.Open 找不到文件时会返回 error,处理 error 时,将 error 的信息打上日志,并且将 err 进行返回,在 main 函数中,拿到 error 后再次打上 error 的日志,这个日志和上面有部分是重复的日志。

在代码调用链多的时候,会打上更多的重复日志,日志中出现非常多的噪音,非常影响排查错误。

代码语言:javascript复制
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)
	}
}

运行输出:

代码语言:javascript复制
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.

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

代码语言:javascript复制
f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open file failed: %w", err)
	}

输出:

代码语言:javascript复制
2022/11/05 17:04:43 open file failed: open test.txt: The system cannot find the
file specified.

fmt.Errorf 返回的是一个新的被包装的 error,errors.Is 可以一层一层的剥开包装来判断是否为原始错误,但是它是做指针判断的,这里 os.Open 返回的原始错误是 os.PathError 但是因为返回的是地址,所以无法用 errors.Is 判断。

代码语言:javascript复制
func main() {

	_, err := ReadFile("test.txt")
	var pathError *os.PathError

	if errors.Is(err, pathError) {
		fmt.Println("is PathError")
	} else {
		fmt.Println("no PathError")
	}
}

输出:

代码语言:javascript复制
no PathError

这里可以用 errors.As 来判断 err 是否为 os.PathError 类型,即使 err 是地址。

这里判断了是否为 os.PathError 错误,并且将返回的 err 转换成了该错误,我们可以调用其中的属性来获取更多的信息。

代码语言:javascript复制
func main() {

	_, err := ReadFile("test.txt")
	var pathError *os.PathError

	if errors.As(err, &pathError) {
		fmt.Println(pathError.Path)
	}

}

输出:

代码语言:javascript复制
test.txt

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

代码语言:javascript复制
func main() {
	_, err := ReadFile("test.txt")
	err = errors.Unwrap(err)
	fmt.Printf("ori err: %v", err)
}

# 5. pkg/errors

上面介绍的都是原生的 errors 处理模块,现在介绍 pkg/errors 模块,完全兼容原生 errors,并且对其进行增强,主要是添加了保存堆栈的能力。

可以使用 errors.Wrap 进行对 error 的包装,并添加额外的信息。

代码语言:javascript复制
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)
	}
}

输出:

代码语言:javascript复制
could not open file: open test.txt: The system cannot find the file specified.

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

代码语言:javascript复制
func main() {
	_, err := ReadFile("test.txt")
	if err != nil {
		fmt.Printf("stack track: n% v", err)
	}
}

输出:

代码语言:javascript复制
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

如果不想保存堆栈信息,只添加额外的信息,可以使用 errors.WithMessage 添加。

代码语言:javascript复制
	f, err := os.Open(path)
	if err != nil {
		return nil, errors.WithMessage(err, "could not open file")
	}

输出:

代码语言:javascript复制
could not open file: open test.txt: The system cannot find the file specified.

还有几个常见的方法

代码语言:javascript复制
// 生成错误的同时带上堆栈信息
func New(message string) error

// 只附加调用堆栈信息
func WithStack(err error) error

// 获得最根本的错误原因
func Cause(err error) error

# 6. error 的最佳实践

处理 error 的方式这么多,我们该如何最优的使用它们呢?有以下几个方法:

  • 在自己的应用代码中,使用 errors.New 或者 errors.Errorf 来返回错误
代码语言:javascript复制
func parseArgs(args []string) error {
	if len(args) < 3 {
		return errors.Errorf("not enough arguments")
	}

	return nil
}
  • 如果调用其他包内的函数,通常简单的直接返回。
代码语言:javascript复制
if err != nil {
	return err
}
  • 如果和其他库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。同样适用于和标准库协作的时候。
代码语言:javascript复制
f, err := os.Open(path)
if err != nil {
	return errors.Wrapf(err, "failed to open %q", path)
  • 直接返回错误,而不是每个错误产生的地方到处打日志。
  • 在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 % v 把堆栈详情记录。
代码语言:javascript复制
func main() {
	err := app.Run()
	if err != nil {
		fmt.Printf("FATAL: % Vn", err)
		os.Exit(1)
	}
}
  • 使用 errors.Cause 获取 root error,再进行和 sentinel error 判定。

# 参考资料

  • io.EOF 设计的缺陷 (opens new window)

0 人点赞