【Golang】异常处理

Posted by 西维蜀黍 on 2020-03-15, Last Modified on 2021-09-21

异常处理

Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 的这种异常处理方式。

因为Go语言的设计者们认为,将异常处理逻辑与业务逻辑混在一起会很容易使得代码变得混乱。因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个异常。

在Go语言中,通过利用多值返回来返回错误。因此,不要用异常代替错误(在 Go 的世界中,我们只能使用 error,而不是 exception),更不要用是否有抛出异常来改变控制程序执行流程(在 Java、C#中我们往往都是这样做的)。在极个别的情况下,也就是说,遇到真正的异常的情况下(比如除数为0了)。才使用Go中引入的Exception处理:defer, panic, recover。

这几个异常的使用场景可以这么简单描述:Go中可以通过调用 panic() 来抛出一个异常(这意味着你可以在任何地方调用 panic() 来表示一个自定义异常),然后通过 defer 关键字声明一个异常处理函数,最后在这个异常处理函数中通过 recover() 捕获任何之前未被捕获的异常,并进行相应的异常处理。

实验

Example 1

package main

import (
	"fmt"
)

func f() {
	defer func() {
		fmt.Println("b")
		if err := recover(); err != nil {
			fmt.Println(err)
		}
		fmt.Println("d")
	}()
	fmt.Println("a")
	panic("a bug occur")
	fmt.Println("c")
}

func main() {
	f()
	fmt.Println("x")
}

//output
a
b
a bug occur
d
x

bx 之前输出,这意味着,当有任何异常抛出时,就会执行通过 defer 声明的对应异常处理函数。

Example 2

package main

import (
	"fmt"
)

func f() {
	defer func() {
		fmt.Println("b")
		if err := recover(); err != nil {
			fmt.Println(err)
		}
		fmt.Println("d")
	}()
	fmt.Println("a")
	fmt.Println("c")
}

func main() {
	f()
	fmt.Println("x")
}

//output
a
c
b
d
x

我们发现,当没有异常被抛出时(输出了 bd),通过 defer 声明的异常处理函数还是会被调用。而被调用的时机是当当前这一层的调用栈被执行完时。

这也意味着,我们最好只是在 if err := recover(); err != nil 的内部添加代码,从而使得通过 defer 声明的异常处理函数就是正在意义上的异常处理函数(因为即使没有出现任何异常时,这个异常处理函数虽然仍然被触发,然后不产生任何 side-effect)。

注意,defer就是用来添加函数结束时执行的语句。

Errors

Introduction

Go code uses error values to indicate an abnormal state. For example, the os.Open function returns a non-nil error value when it fails to open a file.

func Open(name string) (file *File, err error)

The following code uses os.Open to open a file. If an error occurs it calls log.Fatal to print the error message and stop.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

The error type

The error type is an interface type. An error variable represents any value that can describe itself as a string. Here is the interface’s declaration:

type error interface {
    Error() string
}

The most commonly-used error implementation is the errors package’s unexported errorString type.

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

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

How to implement the error interface

A library writer is free to implement this interface with a richer model under the covers, making it possible not only to see the error but also to provide some context. As mentioned, alongside the usual *os.File return value, os.Open also returns an error value. If the file is opened successfully, the error will be nil, but when there is a problem, it will hold an os.PathError:

// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // The associated file.
    Err error    // Returned by the system call.
}

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

PathError’s Error generates a string like this:

open /etc/passwx: no such file or directory

Such an error, which includes the problematic file name, the operation, and the operating system error it triggered, is useful even if printed far from the call that caused it; it is much more informative than the plain “no such file or directory”.

When feasible, error strings should identify their origin, such as by having a prefix naming the operation or package that generated the error. For example, in package image, the string representation for a decoding error due to an unknown format is “image: unknown format”.

Callers that care about the precise error details can use a type switch or a type assertion to look for specific errors and extract details. For PathErrors this might include examining the internal Err field for recoverable failures.

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Recover some space.
        continue
    }
    return
}

The second if statement here is another type assertion. If it fails, ok will be false, and e will be nil. If it succeeds, ok will be true, which means the error was of type *os.PathError, and then so is e, which we can examine for more information about the error.

3 simple ways to create an error

String-based errors

The standard library offers two out-of-the-box options.

// simple string-based error
err1 := errors.New("math: square root of negative number")

// with formatting
err2 := fmt.Errorf("math: square root of negative number %g", x)

Custom errors with data

To define a custom error type, you must satisfy the predeclared error interface.

type error interface {
    Error() string
}

Here are two examples.

type SyntaxError struct {
    Line int
    Col  int
}

func (e *SyntaxError) Error() string {
    return fmt.Sprintf("%d:%d: syntax error", e.Line, e.Col)
}
type InternalError struct {
    Path string
}

func (e *InternalError) Error() string {
    return fmt.Sprintf("parse %v: internal error", e.Path)
}

Demo - 声明 Error

// Error define
var (
	// This error will be return by ConfigRegistry.Get  when the key doesn't called  ConfigRegistry.BindProto
	errConfigNotBound = errors.New("not bind config proto")

	// Config not found
	errConfigNotFound = errors.New("config not found")

	// Config value cannot be unmarshal to the proto bind
	errConfigUnmarshal = errors.New("config unmarshal fail")

	// config key empty
	errNoConfigKey = errors.New("config key empty")

	// instance id empty
	errNoInstanceID = errors.New("instance id empty")

	// Sps not init
	errNotInit = errors.New("not init")

	// Sps initialized
	errInitialized = errors.New("already initialized")

	// Sps not registered
	errNotRegistered = errors.New("not register")

	// Agent already registered: do not register again
	errRegistered = errors.New("already registered")

	// Start/stop over time
	errTimeout = errors.New("time out")
)

func Init(opts ...InitOption) error {
	initLock.Lock()
	defer initLock.Unlock()

	if DefaultAgent != &defaultAgent {
		return errInitialized
	}
return nil
}

Panic

The usual way to report an error to a caller is to return an error as an extra return value. The canonical Read method is a well-known instance; it returns a byte count and an error. But what if the error is unrecoverable? Sometimes the program simply cannot continue.

For this purpose, there is a built-in function panic that in effect creates a run-time error that will stop the program (but see the next section). The function takes a single argument of arbitrary type—often a string—to be printed as the program dies. It’s also a way to indicate that something impossible has happened, such as exiting an infinite loop.

// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
    z := x/3   // Arbitrary initial value
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // A million iterations has not converged; something is wrong.
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

This is only an example but real library functions should avoid panic. If the problem can be masked or worked around, it’s always better to let things continue to run rather than taking down the whole program. One possible counterexample is during initialization: if the library truly cannot set itself up, it might be reasonable to panic, so to speak.

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

Reference