CodeMonkey流水账:Golang的线程(协程)异常处理

没有(try…catch)

当然也可以用go的原理自己写一个try…catch…,这里不讨论。
有兴趣可以搜索一下别人的经验,但是这样会改变用go语言的习惯,我反正不愿意。

错误(Errors)

错误似乎不在我们讨论范围内,因为错误经常是Go的多返回值的一部分,并不是不可预见的异常。对于这种可以预见,可以恢复(继续执行)的错误,官方文档是这么写的:

Library routines must often return some sort of error indication to the caller. As mentioned earlier, Go’s multivalue return makes it easy to return a detailed error description alongside the normal return value. It is good style to use this feature to provide detailed error information. For example, as we’ll see, os.Open doesn’t just return a nil pointer on failure, it also returns an error value that describes what went wrong.

By convention, errors have type error, a simple built-in interface.

type error interface {
    
    
    Error() string
}

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”.

扫描二维码关注公众号,回复: 13478125 查看本文章

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.

异常(Panic & Recover)

除了系统本身一些无法预料的异常,我们也可以手动用panic抛出异常,等待recover处理。
异常可以有类型,panic后就中断了程序的执行,pla,pla,pla…虽然这里管它叫异常,其实英文意思应该是“恐慌”啊,记得么,梅西躺在草坪上说的”我有点慌……“,-__- 官方文档是这么写的:

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

Recover
When panic is called, including implicitly for run-time errors such as indexing a slice out of bounds or failing a type assertion, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way. If that unwinding reaches the top of the goroutine’s stack, the program dies. However, it is possible to use the built-in function recover to regain control of the goroutine and resume normal execution.

A call to recover stops the unwinding and returns the argument passed to panic. Because the only code that runs while unwinding is inside deferred functions, recover is only useful inside deferred functions.

One application of recover is to shut down a failing goroutine inside a server without killing the other executing goroutines.

func server(workChan <-chan *Work) {
    
    
    for work := range workChan {
    
    
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    
    
    defer func() {
    
    
        if err := recover(); err != nil {
    
    
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

In this example, if do(work) panics, the result will be logged and the goroutine will exit cleanly without disturbing the others. There’s no need to do anything else in the deferred closure; calling recover handles the condition completely.

Because recover always returns nil unless called directly from a deferred function, deferred code can call library routines that themselves use panic and recover without failing. As an example, the deferred function in safelyDo might call a logging function before calling recover, and that logging code would run unaffected by the panicking state.

With our recovery pattern in place, the do function (and anything it calls) can get out of any bad situation cleanly by calling panic. We can use that idea to simplify error handling in complex software. Let’s look at an idealized version of a regexp package, which reports parsing errors by calling panic with a local error type. Here’s the definition of Error, an error method, and the Compile function.

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    
    
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    
    
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    
    
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
    
    
        if e := recover(); e != nil {
    
    
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}

If doParse panics, the recovery block will set the return value to nil—deferred functions can modify named return values. It will then check, in the assignment to err, that the problem was a parse error by asserting that it has the local type Error. If it does not, the type assertion will fail, causing a run-time error that continues the stack unwinding as though nothing had interrupted it. This check means that if something unexpected happens, such as an index out of bounds, the code will fail even though we are using panic and recover to handle parse errors.

With error handling in place, the error method (because it’s a method bound to a type, it’s fine, even natural, for it to have the same name as the builtin error type) makes it easy to report parse errors without worrying about unwinding the parse stack by hand:

if pos == 0 {
    
    
    re.error("'*' illegal at start of expression")
}

Useful though this pattern is, it should be used only within a package. Parse turns its internal panic calls into error values; it does not expose panics to its client. That is a good rule to follow.

By the way, this re-panic idiom changes the panic value if an actual error occurs. However, both the original and new failures will be presented in the crash report, so the root cause of the problem will still be visible. Thus this simple re-panic approach is usually sufficient—it’s a crash after all—but if you want to display only the original value, you can write a little more code to filter unexpected problems and re-panic with the original error. That’s left as an exercise for the reader.

一般的异常处理

上面我懒得写了所以抄了官方文档,那么看完上面的,我们基本上也明白了一般来说,main函数开始需要定义一个处理日常的方法,用来处理整个后续函数出现的异常:

func main() {
    
    
	defer func() {
    
    
		if err := recover(); err != nil {
    
    
			//记录或者汇报异常
			...
			os.Exit(1)
		}
	}()
	//然后开始真正的main函数
	...
}

线程(协程)异常处理

但是如果把官方文档看得仔细一点,或者实际测试一下。
就会发现线程内部抛出的异常,main函数最开始的这个方法是处理不了的。
也就是说每个线程开始都需要定义一次

func dosometing() {
    
    
	defer func() {
    
    
		if err := recover(); err != nil {
    
    
			//记录或者汇报异常
			...
			os.Exit(1) // 或者别的什么返回
		}
	}()
	//然后开始真正的dosomething函数
	...
}

...
//在别处用线程调用这个方法
go dosomethion()
...

如果每个函数有自己的处理方式,返回值,那么只能一个个写。
但是如果这些异常出现后,程序都无法正常继续,不如写一个全局的处理异常方法:

func _exceptions() {
    
    
	if err := recover(); err != nil {
    
    
		//记录或者汇报异常
		...
		os.Exit(1) // 或者别的什么返回
	}
}
func dosometing() {
    
    
	defer _exceptions()
	//然后开始真正的dosomething函数
	...
}

func main() {
    
    
	defer _exceptions()
	//然后开始真正的main函数
	...
	go dosomethion()
	//同理 do anotherthing()
	...
}

这样就不需要想官方文档那样,每个do方法,都有个safelyDo方法套在外面。
对了,上面代码的前提:这些函数的异常处理结果都是退出程序。
当然,改别的也行,统一处理就行。

猜你喜欢

转载自blog.csdn.net/ddrfan/article/details/112832572