Golang correct posture for error and exception handling

Source: Internet
Author: User
This is a creation in Article, where the information may have evolved or changed.

Preface

Errors and exceptions are two different concepts that are very easy to confuse. Many programmers are accustomed to seeing all anomalies as errors, without distinguishing between errors and exceptions, and, even if exceptions are thrown in the program, they are caught and converted into errors in a timely manner. On the surface, the idea of everything being wrong is simpler, and the introduction of exceptions only adds extra complexity.
But that is not the case. As we all know, Golang follows the design philosophy of "Less is more" and pursues simplicity and elegance, that is, if the unusual value is small, the exception will not be added to the language features.

Error and exception handling is an important part of the program, let's look at some of the following:

    1. How are errors and exceptions differentiated?
    2. What are the different ways to handle errors?
    3. When do I need to use the exception termination program?
    4. When do I need to catch an exception?
    5. ...

If your answer to these questions is not too clear, then take a moment to read this article, perhaps to give you some inspiration.

Face-to-exception.png

Basic knowledge

Error refers to a problem where there may be a problem, such as the failure to open a file, which is expected, and the exception refers to a problem where there should be no problem, such as referencing a null pointer, which is unexpected. It can be seen that the error is part of the business process and the exception is not.

The error interface type is introduced in Golang as the standard mode for fault handling, and if the function returns an error, the return value type list must contain error. The error process is similar to the code in the C language, and can be returned on a level-by-layer basis until it is processed.

The Golang introduces two built-in functions panic and recover to trigger and terminate the exception-handling process, while introducing the keyword defer to defer execution of the function behind defer.
Wait until the function that contains the defer statement is complete, the deferred function (the function after defer) is executed, regardless of whether the function that contains the defer statement ends with the normal end of the return, or because of the exception caused by panic. You can execute multiple defer statements in a function that are executed in the opposite order as they are declared.
When the program runs, if it encounters a reference null pointer, subscript out of bounds, or explicitly call the panic function, then the execution of the panic function is triggered first and then the deferred function is called. The caller continues to pass panic, so the process has repeatedly occurred in the call stack: The function stops executing, calls the deferred execution function, and so on. If there is no call to the recover function in the delay function, it will reach the beginning of the Ctrip, the Ctrip end, and then terminate all other Ctrip, including the main Ctrip (similar to the C language of the main thread, the Ctrip ID is 1).

Errors and exceptions from the Golang mechanism, is the difference between error and panic. Many other languages are the same, such as C++/java, without error but with errno, without panic but with throw.

Golang errors and exceptions can be converted to each other:

    1. Error forwarding exception, such as the program logic to try to request a URL, up to three attempts, the attempt three times the request failed is an error, after the third attempt is unsuccessful, the failure is promoted to an exception.
    2. Exception-forwarding errors, such as panic-triggered exceptions, are assigned to variables of the error type in the return value after recover is restored so that the upper function continues the error-handling process.

A revelation

The RegExp package has two functions compile and mustcompile, which are declared as follows:

func Compile(expr string) (*Regexp, error)func MustCompile(str string) *Regexp

The same features, different designs:

    1. The compile function compiles the regular expression into a valid, matching format based on the error-handling design and is suitable for user input scenarios. When the user enters a regular expression that is not valid, the function returns an error.
    2. The Mustcompile function is based on the exception handling design and is suitable for hard-coded scenarios. It is unnecessary and cumbersome for callers to be asked to examine the error when the caller knows explicitly that the input does not cause a function error. We should assume that the input of the function is always valid, and the panic exception is triggered when the caller enters an input that should not be present.

So we get a revelation: under what circumstances to use the wrong expression, under what circumstances with an abnormal expression, there is a set of rules, otherwise it is very easy to appear all wrong or everything is abnormal situation.

In this revelation, we give the scope of exception handling (scenario):

    1. Null pointer reference
    2. Subscript out of Bounds
    3. Divisor is 0
    4. Branches that should not appear, such as default
    5. Input should not cause a function error

In other scenarios we use error handling, which makes our function interfaces very refined. For exceptions, we can choose to go upstream in a suitable recover and print the stack information so that the deployed program does not terminate.

Description: Golang error handling method has been a lot of people criticized the place, some people say that half of the code is "if err! = Nil {/print && error handling/}", seriously affecting the normal processing logic. When we distinguish between errors and anomalies, the design of the function according to the rules will greatly improve readability and maintainability.

Correct posture for error handling

Posture One: The failure causes only one time, do not use the error

Let's look at a case:

func (self *AgentContext) CheckHostType(host_type string) error { switch host_type { case "virtual_machine": return nil case "bare_metal": return nil } return errors.New("CheckHostType ERROR:" + host_type)}

As we can see, the function fails for only one reason, so the type of the return value should be bool instead of error, refactoring the code:

func (self *AgentContext) IsValidHostType(hostType string) bool { return hostType == "virtual_machine" || hostType == "bare_metal"}

Description: In most cases, the cause of the failure is more than one, especially for I/O operations where the user needs to know more about the error message, when the return value type is no longer a simple bool, but an error.

Posture Two: Do not use error when no failure

Error is so popular in Golang that many people design functions regardless of whether 3,721 uses error, even if there is no reason for failure.
Let's take a look at the sample code:

func (self *CniParam) setTenantId() error { self.TenantId = self.PodNs return nil}

For the above function design, there will be the following calling code:

err := self.setTenantId()if err != nil {    // log    // free resource    return errors.New(...)}

Based on our correct posture, refactor the code:

func (self *CniParam) setTenantId() { self.TenantId = self.PodNs}

The calling code then changes to:

self.setTenantId()

Posture Three: Error should be placed at the end of the return value type list

For return value type error, it is used to pass an error message, which is usually placed in the last Golang.

resp, err := http.Get(url)if err != nil {    return nill, err}

BOOL is the same as the return value type.

if !ok {    // ...cache[key] does not exist… }

Posture Four: Error values are defined uniformly, not followed by feelings.

Many people write code and return errors everywhere. New (value), and error value may be different in the expression of the same meaning, such as "record does not exist" error value may be:

    1. "Record is not existed."
    2. "Record is not exist!"
    3. "# # #record is not existed!!! "
    4. ...

This allows the same error value to be scattered in a large piece of code, and when the upper function wants to uniformly handle a particular error value, it is necessary to roam all the underlying code to ensure that the error value is unified, and unfortunately sometimes there is a slip through, and this way seriously hinders the refactoring of the wrong value.

As a result, we can refer to the error code definition file of C + + and add an Error object definition file to each package in Golang, as follows:

var ERR_EOF = errors.New("EOF")var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")var ERR_SHORT_BUFFER = errors.New("short buffer")var ERR_SHORT_WRITE = errors.New("short write")var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

Description: The author prefers C + + for "All Caps + underline segmentation" for constants, and readers can customize them according to the team's naming conventions or personal preferences.

Posture Five: When the error is passed layer by level, logs are added to the layer

According to the author's experience, the layers are added to the log very convenient fault location.

Description: As for testing to find faults, not logs, many teams are still having trouble doing so. If you or your team can do so, ignore this posture:)

Posture Six: error handling using defer

We generally deal with errors by judging the value of the error, and if the current operation fails, you need to destroy the resource for the create in this function, the sample code is as follows:

FuncDeferdemo () error {err: = CreateResource1 () if Err! = nil {return err_create_resource1_failed} ERR = CreateResource2 () if err! = nil {DestroyResource1 () return err_create_resource2_failed} err = CreateResource3 () if ERR! = nil {DestroyResource1 () DestroyResource2 () return Err_create_ resource3_failed} err = CreateResource4 () if err! = nil { DestroyResource1 () DestroyResource2 () destroyResource3 () return err_create_resource4_ FAILED} return nil}      

When the Golang code executes, it is pressed into the stack if it encounters a defer closure call. When the function returns, the closure is called in the order of last-in-first-out.
The argument for a closure is a value pass, whereas for an external variable it is a reference pass, so the value of the external variable err in the closure becomes the most recent err value when the external function returns.
Based on this conclusion, we refactor the example code above:

FuncDeferdemo()Error {ERR: = CreateResource1 ()If Err! =Nil {Return err_create_resource1_failed}DeferFunc() {If Err! =Nil {DestroyResource1 ()}} () Err = CreateResource2 ()If Err! =Nil {Return err_create_resource2_failed}defer func  () {if err! = nil {DestroyResource2 ()}} () Err = CreateResource3 () if err! = nil {return err_create_resource3_failed} defer func () {if err! = nil {DestroyResource3 ()}} () Err = CreateResource4 () if err! = nil {return err_create_resource4_failed} return  Nil}                 

Posture VII: Do not immediately return an error when you try to avoid failure several times

If the occurrence of an error is accidental, or is caused by an unpredictable problem. A wise choice is to retry the failed operation, sometimes succeeding the second or third attempt. When retrying, we need to limit the retry interval or the number of retries to prevent unlimited retries.

Two cases:

    1. We usually surf the internet, try to request a URL, sometimes the first time there is no response, when we refresh again, there is a surprise.
    2. A QA team has suggested that when Neutron's attach operation fails, it is best to try it three times, which is validated in the circumstances.

Posture Eight: It is recommended not to return an error when the upper function does not care about errors

For some resource cleanup-related functions (destroy/delete/clear), if a child function goes wrong, print the log without further feedback to the upper function, because in general, the upper function is not concerned with the result of execution, or even if there is nothing to care about, We therefore recommend that the correlation function be designed to not return error.

Posture IX: A useful return value is not ignored when an error occurs

Typically, when a function returns NON-NIL error, the other return values are undefined (undefined), and these undefined return values should be ignored. However, there are a few functions that still return some useful return values when an error occurs. For example, when an error occurs reading a file, the Read function returns the number of bytes that can be read and the error message. In this case, you should print the string that you read along with the error message.

Description: The return value of the function should be clearly stated so that it can be used by other people.

Correct posture for exception handling

Posture One: In the program development phase, adhere to the speed error

When learning Erlang last year, the idea of a quick mistake was set up, which simply means "Let it hang" and only if you hang up will you know the error the first time. In the early stages of development and before any release phase, the simplest and probably best method is to call the panic function to interrupt the execution of the program to force an error, so that the error is not ignored and can be repaired as soon as possible.

Posture Two: After the program is deployed, the exception should be resumed to avoid program termination

In Golang, although there are goroutine similar to the Erlang process, it is necessary to emphasize that Erlang hangs, only the abnormal exit of the Erlang process, will not cause the entire Erlang node to exit, so it hangs the impact level is relatively low, If the Goroutine panic, and there is no recover, then the entire Golang process (like Erlang nodes) exits unexpectedly. Therefore, once the Golang program is deployed, the exception that occurs in any situation should not cause the program to exit unexpectedly, we add a deferred recover call in the upper function to achieve this, and whether the recover needs to be determined according to the environment variable or configuration file, Recover is required by default.
This posture is similar to assertions in C, but there are differences: Generally in release versions, assertions are defined as null and fail, but need to have an if check exists for exception protection, although this is not recommended in contract design. In Golang, recover can completely terminate the abnormal unfolding process, saving time and effort.

We should respond in the most reasonable way in calling recover's delay function:

    1. Print the exception call information for the stack and key business information so that these issues remain visible;
    2. The exception is converted to an error so that the caller can restore the program to a healthy state and continue to run safely.

Let's look at a simple example:

FuncFunca()Error {DeferFunc() {If p: =Recover (); P! =Nil {fmt. Printf ("Panic recover! P:%v ", p) Debug. Printstack ()}} () return FUNCB ()}func funcb() error { //simulation Panic ("foo") re Turn errors. New ("Success")}func Test() {err: = Funca () If err = = Nil {fmt. Printf ("err is nil\\n")} else {fmt. Printf ("Err is%v\\n", Err)}}             

We expect the output of the test function to be:

err is foo

The output of the test function is actually:

nil

The reason is that the panic exception handling mechanism does not automatically pass the error message to the errors, so the code is passed explicitly in the Funca function, as shown here:

func funcA() (err error) { defer func() { if p := recover(); p != nil { fmt.Println("panic recover! p:", p) str, ok := p.(string) if ok { err = errors.New(str) } else { err = errors.New("panic") } debug.PrintStack() } }() return funcB()}

Posture Three: Use exception handling for branches that should not appear

When something should not happen, we should call the panic function to trigger the exception. For example, when a program reaches a path that is logically impossible to reach:

switch s := suit(drawCard()); s {    case "Spades":    // ...    case "Hearts": // ... case "Diamonds": // ... case "Clubs": // ... default: panic(fmt.Sprintf("invalid suit %v", s))}

Posture Four: a function that should not have a problem with the entry parameter, using panic design

The entry should not have a problem generally refers to hard coding, we first look at the "one Revelation" section mentioned in the two functions (compile and Mustcompile), wherein the Mustcompile function is the compile function of the wrapper:

func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp}

Therefore, for cases where both user input and hard-coded scenarios are supported, the function that supports hard-coded scenes is the wrapper that supports user input scene functions.
For cases where only hard-coded single scenes are supported, the function is designed to use panic directly, that is, there will be no error in the return value type list, which makes the function call handling very convenient (without the tedious "if err! = Nil {/print && error handling/}" code block).

Summary

This article takes Golang as an example, explains the difference between errors and anomalies, and shares a lot of errors and abnormal handling of the correct posture, these postures can be used alone, can also be combined to use, I hope to have a little inspiration for everyone.


Original: Http://www.jianshu.com/p/f30da01eea97
Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.