Error handling in Go language

Source: Internet
Author: User
Tags key string stack trace


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:


    1. 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.
    2. 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


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.