error

下面是Go标准库对于error的定义

type error interface {
	Error() string
}

可以看到Go中的error就是一个普通的接口,定义了一个Error()方法。
更多细节如下

type errorString struct {
	s string
}
func (e *errorString) Error() string {
	return e.s
}
errors.New()
func New(text string) error {
	return &errorString{text}
}
  • C返回值为int,代表成功或者失败。(由于redis底层由C实现,因此异常处理同上)
  • C++引入exception,但是无法知晓被调用者(换个说法平时调用的第三方库中的函数)会抛出什么异常。
  • Java引入checked exception,方法的所有者必须声明,调用者必须处理异常。启动时,可能抛出大量异常。
    Go不同于以上的语言,未引入exception,而是实现了error interface的对象,交由调用者来判断定,相当于把exception扔给调用者处理。Go中有panic机制,panic意味着代码无法继续运行下去。(通常是重大错误导致程序已经无法正常继续下去,才会panic)如下。
func main() {
	defer fmt.Println("can happen")
	l := "Hello"
	fmt.Println(l[6])
	fmt.Println("can't happen")
}

请添加图片描述

上图可以看到打印出的错误原因以及堆栈信息,报错后程序直接退出,后续的代码不继续执行(derfer中的函数能正常执行)。
对于真正意外的情况, 那些表示不可恢复的程序错误, 例如索引越界、 不可恢复的环境问题、 栈溢出,我们才使用 panic 。对于其他的错误情况, 我们应该是期望使用 error 来进行判定。
想要程序触发panic后正常退出,可以使用recover()如下。

func main() {
	defer func() {
		if recover() != nil {
			fmt.Println("normal exit")
		}
	}()
	panic("this is panic")
}

请添加图片描述

error type

sentinel error

if err == ErrSomething { ... }
  • Sentinel errors成为你API公共部分。我们应该把特定值的错误,写入api文档中暴露给调用者。如果API定义了一个返回特定错误的interface, 则该接口的所有实现都将被限制为仅返回该错误,即使它们可以提供更具描述性的错误。如io.EOF,io.Copy这类函数需要reader的实现者,比如返回 io.EOF来告诉调用者没有更多数据了, 但这又不是错误。
  • Sentinel errors在两个包之间创建了依赖 。当你要检查错误时,必须要导入第三方包。如检查错误是否等于io.EOF ,您的代码必须导入io包 。

结论:我们应该尽量避免在编程时使用sentinel errors。

error types

Error type是实现了error接口的自定义类型。如下面定义的MyError类型记录了文件和行号以及错误信息。

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)
}

func test() error {
   return &MyError{ "Something happened", "server.go",  42 }
}

因为MyError是一个type,调用者可以使用断言转换这个类型,来获得更多上下文信息。

func main() {
	err := test()
	switch err := err.(type) {
		case nil:
		// call succeeded, nothing to do
		case *MyError:
			fmt.Println("error occurred on line:", err.Line)
   		default:
   		// unknown error
   		}
}

与特定的错误值相比,错误类型能提供更多信息。如下面的例子,os包中的os.PathError。

type PathError struct {
	Op string
	Path string
	Err error
}

func (e *PathError) Error() string

结论:调用者要使用类型断言和类型switch, 就要让自定义的error变为public。这种模型会导致和调用者产生强耦合, 从而导致API变得脆弱。所以应尽量避免使用error types,虽然错误类型比sentinel errors更好,因为它们可以捕获关于出错的更多上下文,但是error types共享error values许多相同的问题。因此,尽量避免自定义错误类型,或者至少避免将它们作为公共 API 的一部分。

opaque errors

if err != nil { return err }
type temporary interface {
	Temporary() bool
}

// IsTemporary returns true if err is temporary
func IsTemporary(err error) bool {
	te, ok := err.(temporary)
	return ok && te.Temporary()
}
package net

type Error interface {
	error
	Timeout() bool
	Temporary() bool
}
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
	time.Sleep(1e9)
	continue
}
if err != nil {
	log.Fatal(err)
}

Assert errors for behaviour, not type

结论:应该尽量采用这种方式进行错误处理。

handling error

下面是使代码更加优雅的一些方法。

eliminate error handling by eliminating errors

我们在写HTTP报文的时候,通常可能会采取下面这种写法。

type Header struct {
	Key, Val string
}

type Status struct {
	Code int
	Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Headers, 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.Val)
	if err != nil {
		return err
	}
}
if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
	return err
}

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

可以看到代码读起来太冗杂了。我们可以以下面这种方式实现。

type errWriter struct {
	io.Writer
	err error
}
func (e *errWriter) 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, nil
}
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
	ew := &errWriter{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.Val)
	}
	fmt.Fprint(ew, body)

	return ew.err
}

wrap errors

"No such file or directory"fmt.Errorf()

you should only handle errors once. Handling an error means inspecting the error value, and making a single decision.

对于Go语言的错误处理,我们期待的是仅处理一次(直接返回error或者打印错误等),如果在两个或者多个地方都打印错误,会导致错误打印到处都是,不易于错误处理。
三个需要注意的要点:

  • 错误要被日志记录。
  • 应用程序处理错误,保证100%完整性。
  • 之后不再报告当前错误。

综合上面一系列问题,以下是我们应当采取的正确处理方式。
通过github.com/pkg/errors包,来帮助我们更好的处理错误。

errors.Wrap(err, "more informations")errors.WithStack(err)errors.New("can't work")if err != nil { return err }%+vfmt.Printf("Fatal: %+v\n", err)if errors.Is(err, Errsomething) { ... }if errors.As(err, &e) { ... }
fmt.Errorf("more information %w", err)