Preface
Error handling is an important element in every language. As we all know, there are two kinds of exceptions and errors commonly encountered in writing programs, and Golang is no exception. Golang follow the "Less is more" philosophy of design, error handling also strive to be concise and clear, in error handling, using a similar C language error treatment scheme, in addition to the error also has an anomaly concept, Golang introduced two built-in functions panic and recover to trigger and terminate the exception processing process.
Basic knowledge
Error refers to a problem where there may be a problem, such as the possibility of failure when opening a file, which is expected, and the exception refers to a problem where there should be no problem, such as a reference to a null pointer, which is unexpected. It is visible that the error is part of the business logic and the exception is not.
We know that in the C language is to return 1 or null and other information to express errors, but for the user, do not look at the corresponding API documentation, it is not clear what this return value means, such as return 0 is a success or failure? In the case of a golang in which the error interface type is introduced as the standard mode of fault handling, if the function is to return an error, the return value type list must contain the two built-in functions panic and recover in the Error;golang to trigger and terminate the exception-handling process. The keyword defer is also introduced 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 there is a null pointer reference, array subscript out of bounds and other anomalies, it will trigger the execution of the panic function in Golang, the program will break the run, and immediately execute the function that is delayed in the goroutine, if you do not capture, the program will crash.
Errors and exceptions from the Golang mechanism, is the difference between error and panic. Many other languages are the same, such as C++/java, there is no error but there is errno, there is no panic but there is a throw, but the panic of the application of a few different. Because panic can cause a program to crash, panic is generally used for critical errors.
Error handling
We write a simple program that attempts to open a nonexistent file:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println("error:",err)
return
}
fmt.Println(f.Name(), "open successfully")
}
You can see that our program called the OS Package's Open method, which is defined as follows:
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
The reference note can be used to know if this method returns a readable file handle and a value of nil error, if the method does not successfully open the file returns an error of type *patherror.
If a function or method returns an error, the error is returned as the last value according to the convention of Go. The Open function also returns err as the last return value.
In the go language, errors are usually handled by comparing the returned error to nil. A nil value indicates that no error occurred, not a nil value indicating an error occurred. So there's a line of code above US:
if err != nil {
fmt.Println("error:",err)
return
}
If you read a project in any of the go languages, you'll find code like this everywhere, and the go language handles errors in the code in this simple form.
We performed in playground and found that the results showed
error: open /test.txt: No such file or directory
We can find that we have effectively detected and handled the error caused by opening a nonexistent file in the program, in the example we just output the error and return it.
The above mentioned the Open method error will return a *patherror type of error, what is the type specific situation? Don't worry, let's start by looking at how the error in Go is implemented.
Error type
What is the error type returned in go? Look at the source found that the error type is a very simple interface type, specifically as follows
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
Error has a method that is signed as error () string. All types that implement the interface can be treated as an error type. The error () method gives an incorrect description.
Fmt. PRINTLN when printing an error, the error () string method is called internally to get a description of the fault. The error description in the previous section of the example is printed in this way.
Custom error types
Now we go back to the *patherror type in the code, first of all obvious OS. The error returned by the Open method is a type of err, so we can know that the Patherror type must implement the error type, that is, the error method is implemented. Now let's look at the concrete implementation
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
You can see that the Patherror type implements the error method, which returns the concatenation return value of the file operation, path, and error string.
Why do I need to customize the type of error? If a mistake we get is just the wrong string description, it's obviously not possible to get more information from the error or to do some logic-related validation, so that we can customize the wrong structure by implementing the error () To make the struct an error type, using the type recommendation, we can do some work such as logical check or error classification from the returned error through some members of the struct. For example:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err, ok := err.(*os.PathError); ok {
fmt.Println("File at path", err.Path, "failed to open")
return
}
fmt.Println(f.Name(), "opened successfully")
}
By inferring the error type to the actual patherror type in the code above, we can get the data of the OP, path and so on, which is more helpful to the error handling in the actual scene.
Our group now pulls through a set of error types and error code specifications, the previous project was written in the code in the controller to return according to different circumstances, this processing method has many shortcomings, such as the lower layer only returns an error type, the upper how to determine what error is the error, What kind of error code should I use? In addition, the program relies on programmers to write dead some logic error code for XXXX, so that the program lacks stability, error code return is also more arbitrary, so I also went to customize the error, as follows:
var (
ErrSuccess = StandardError {0, "Success"}
ErrUnrecognized = StandardError {-1, "Unknown Error"}
ErrAccessForbid = StandardError {1000, "No access rights"}
ErrNamePwdIncorrect = StandardError {1001, "Wrong username or password"}
ErrAuthExpired = StandardError {1002, "Certificate expired"}
ErrAuthInvalid = StandardError {1003, "Invalid signature"}
ErrClientInnerError = StandardError {4000, "Client internal error"}
ErrParamError = StandardError {4001, "Parameter error"}
ErrReqForbidden = StandardError {4003, "The request was denied"}
ErrPathNotFount = StandardError {4004, "The request path does not exist"}
ErrMethodIncorrect = StandardError {4005, "Request method error"}
ErrTimeout = StandardError {4006, "Service timed out"}
ErrServerUnavailable = StandardError {5000, "Service is unavailable"}
ErrDbQueryError = StandardError {5001, "Database query error"}
)
// StandardError standard error, including error code and error message
type StandardError struct {
ErrorCode int `json:" errorCode "`
ErrorMsg string `json:" errorMsg "`
}
// Error implements the Error interface
func (err StandardError) Error () string {
return fmt.Sprintf ("errorCode:% d, errorMsg% s", err.ErrorCode, err.ErrorMsg)
}
In this way, you can know the error message and error code that should be returned by directly taking StandardError errorcode, and it is convenient to call, and it is standardized to solve the problem of error handling in the previous project.
Assertion Error Behavior
Sometimes just asserting that a custom error type might not be convenient in some cases, you can get more information by invoking a custom error method, such as Dnserror in a net package in a standard library
type DNSError struct {
Err string // description of the error
Name string // name looked for
Server string // server used
IsTimeout bool // if true, timed out; not all timeouts set this
IsTemporary bool // if true, error is temporary; not all errors set this
}
func (e *DNSError) Timeout() bool { return e.IsTimeout }
func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }
You can see that you have not only customized the Dnserror error type, but also added two methods for the error to let the caller determine whether the error is a temporary error or is caused by a timeout.
package main
import (
"fmt"
"net"
)
func main() {
addr, err := net.LookupHost("gygolang.com")
if err, ok := err.(*net.DNSError); ok {
if err.Timeout() {
fmt.Println("operation timed out")
} else if err.Temporary() {
fmt.Println("temporary error")
} else {
fmt.Println("generic error: ", err)
}
return
}
fmt.Println(addr)
}
In the above code, we tried to get the IP of golangbot123.com (invalid domain name). And then through *net. Dnserror the type assertion, gets the underlying value of the error. It then checks with the wrong behavior whether the error was caused by a time-out or a temporary error.
Exception handling
When to use panic
It is important to note that you should use the error as much as possible instead of using panic and recover. The panic and recover mechanisms should be used only when the program cannot continue to run.
Panic has two reasonable use cases:
- An unrecoverable error has occurred and the program cannot continue running. One example is that the Web server cannot bind the required ports. In this case, you should use panic, because if you can't bind a port, you can't do anything.
- A programming error has occurred. If we have a method that receives pointer parameters, others call it using nil as a parameter. In this case, we can use panic because this is a programming error: A method called with the nil parameter that can only receive a legitimate pointer.
Panic
The built-in panic function is defined as follows
func panic(v interface{})
When the program terminates, the parameters of the incoming panic are printed. Let's look at an example and deepen our understanding of panic.
package main
import (
"fmt"
)
func fullName(firstName *string, lastName *string) {
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
firstName := "foo"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
The above program is very simple, if FirstName and LastName have any of the empty program will panic and print out different information, the program output is as follows:
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0x1042ff98, 0x0)
/tmp/sandbox038383853/main.go:12 +0x140
main.main()
/tmp/sandbox038383853/main.go:20 +0x40
When panic occurs, the program terminates, prints the parameters of the incoming panic, and then prints out the stack trace. The program will first print out the information about the incoming panic function:
panic: runtime error: last name cannot be nil
It then prints the stack information, first printing the first item in the stack
main.fullName(0x1042ff98, 0x0) /tmp/sandbox038383853/main.go:12 +0x140
Then print the next item in the stack
main.main() /tmp/sandbox038383853/main.go:20 +0x40
In this example, this is the top of the stack and ends the printing.
Delay function When panic occurs
When a function occurs panic, it terminates the run, and after all the deferred functions are executed, the program control returns to the caller of the function. This process persists until all functions of the current process return exit, and the program prints out the panic information, prints out the stack trace, and finally terminates the program .
In the example above, we have no delay in invoking any functions. If there is a delay function, it is called first, and then program control is returned to the function caller. Let's modify the example above, using a deferred statement.
package main
import (
"fmt"
)
func fullName(firstName *string, lastName *string) {
defer fmt.Println("deferred call in fullName")
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
defer fmt.Println("deferred call in main")
firstName := "foo"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
You can see the output as follows:
deferred call in fullName
deferred call in main
panic: runtime error: last name cannot be nil
goroutine 1 [running]:
main.fullName(0x1042ff90, 0x0)
/tmp/sandbox170416077/main.go:13 +0x220
main.main()
/tmp/sandbox170416077/main.go:22 +0xc0
The delay function is executed before the program exits.
Recover
The program crashes after panic, recover is used to regain control of the panic process. The built-in recover function is defined as follows
func recover() interface{}
It is only useful to invoke recover within the deferred function. Call recover within the delay function, you can fetch the panic error message, and stop the Panic Continuation event (panicking Sequence), the program runs back to normal. If you call recover outside of the deferred function, you cannot stop the panic continuation event.
Let's change the program and use recover to get back to normal operation after panic occurs.
package main
import (
"fmt"
)
func recoverName() {
if r := recover(); r!= nil {
fmt.Println("recovered from ", r)
}
}
func fullName(firstName *string, lastName *string) {
defer recoverName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}
func main() {
defer fmt.Println("deferred call in main")
firstName := "foo"
fullName(&firstName, nil)
fmt.Println("returned normally from main")
}
When fullName occurs panic, the deferred function Recovername () is called, which uses recover () to stop the Panic continuation event. The program will output
recovered from runtime error: last name cannot be nil
returned normally from main
deferred call in main
When a program occurs panic, the deferred function recovername is called, which in turn calls recover () to regain control of the panic process. After recover () is executed, the panic stops, and the program control returns to the caller (here is the main function), which continues to run normally after the panic has occurred. The program prints returned normally from main, followed by deferred call in main.
Run-time Panic
Run-time errors can also cause panic. This is equivalent to calling the built-in function panic, whose parameters are run by the interface type. The Error is given.
package main
import (
"fmt"
)
func a() {
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}
The above code is a typical array of panic caused by the cross-border, the program output is as follows:
panic: runtime error: index out of range
goroutine 1 [running]:
main.a()
/tmp/sandbox100501727/main.go:9 +0x20
main.main()
/tmp/sandbox100501727/main.go:13 +0x20
You can see that there is nothing different from the panic we just started manually, but it will print a run-time error.
Is it possible to restore a runtime panic? Of course it is possible, just like the method of recovering panic, call recover in the delay function:
package main
import (
"fmt"
)
func r() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}
func a() {
defer r()
n := []int{5, 7, 4}
fmt.Println(n[3])
fmt.Println("normally returned from a")
}
func main() {
a()
fmt.Println("normally returned from main")
}
Conversion of errors and anomalies
Errors and anomalies can sometimes be converted,
- 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.
- 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.
For example, there are two functions in the gin framework used in our project:
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
value, exists = c.Keys[key]
return
}
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) interface{} {
if value, exists := c.Get(key); exists {
return value
}
panic("Key \"" + key + "\" does not exist")
}
You can see the same features in different designs:
- The Get function is based on an error design and returns an error of type BOOL If a parameter cannot be taken in the user's argument.
- Mustget is based on the exception design, if it is not possible to fetch a parameter program will panic, used to force a hard-coded scene to take a parameter.
You can see that errors and anomalies can be converted, specifically how to convert to see business scenarios to be determined.
How to handle errors correctly and gracefully
The error should be placed at the end of the return value type list.
It is very non-conforming to see that there is an error in the project in the middle or the first one is returned.
Error values are defined uniformly, rather than being written as you want.
Refer to the previous section of our group Neraton error codes and error messages.
Do not ignore errors
There may be times when some programmers make lazy writing code like this.
foo, _ := getResult(1)
Ignoring the error, there is no need to verify, but it is very dangerous, once a certain error is ignored, it is likely to cause the following program bugs or even direct panic.
Do not go directly to verify the error string
For example, our earliest OS. The open function, we go to verify the error can write like this?
if err.Error == "No such file or directory"
This obviously does not, the code is very bad, and the character judgment is not insurance, how to do? Use the custom error described above.
Summary
In this paper, the concept of error and anomaly in go and its processing method are described in detail, and we hope to inspire you.
Resources
https://studygolang.com/articles/12785