This is the 26th day of my participation in the August More Text Challenge
Golang’s error handling may differ from that of other mainstream languages such as Javascript, Java, and Python. So we came up with the idea to talk about golang’s error handling and how it should be handled in real development. Because sharing has a basic understanding developers for Golang, some simple parts will not be described.
How to define an error
In Golang, errors are treated as values, both during type checking and compilation, no different from string or integer values. Declaring a string variable is no different from declaring an error variable.
You can define the interface as the type of error, and you’re up to yourself in terms of what information an error can provide. That’s the benefit of error as a value in Golang, but it’s also up to the developer who defined it to be good or bad. That is to say, error incorporates too many artificial subjective factors.
package main
import (
"fmt"
"io/ioutil"
)
func main(a){
dir, err := ioutil.TempDir(""."temp")
iferr ! =nil{
fmt.Errorf("failed to create temp dir: %v",err)
}
}
Copy the code
The importance of errors in language
Error handling design has long been a popular topic of discussion in Go. Error handling is the core of the language, but the language does not specify how to handle errors. The community has made efforts to improve and standardize error handling, but many people overlook the central role of errors in our application domain. That said, errors are just as important as the customer and order type.
Error in Golang
An error indicates that an unnecessary condition has occurred in the application. For example, you want to create a temporary directory where you can store some files for your application, but the directory fails to be created. This is an unexpected situation, which can be represented by an error.
By creating custom errors, richer error information can be passed to the caller. Returns the error to the caller to handle the error. Golang itself allows functions to have multiple return values, so errors are usually returned to the caller as the last argument to the function to handle.
The errors are I/O
- Sometimes developers are producers of errors (writing errors)
- Sometimes developers are error consumers (reading error)
So part of what we’re doing in our development program is reading and writing errors
Context of Errors
What is the context of error? There are a couple of things to consider when you define an error, like how we define an error and how we handle an error in different programs, right
- CLI tool
- library
- A long-running system
And we need to think about who’s using the program, how they’re using the system, and that’s what we need to think about when we design and define error messages.
Wrong type
As for the error core, the error may be the one we expect, or the error may be the one we do not take into account, such as invalid memory, out-of-bounds array, that is, the error can not be solved by the code itself temporarily, such errors often make the code Panic, so Panic. Usually such errors are catastrophic failures for the program and cannot be fixed.
Custom error
As mentioned earlier, errors are represented by built-in error interface types, which are defined as follows.
type error interface {
Error() string
}
Copy the code
Structs that implement error () are structs that implement error ()
type SyntaxError struct {
Line int
Col int
}
func (e *SyntaxError) Error(a) string {
return fmt.Sprintf("%d:%d: syntax error", e.Line, e.Col)
}
Copy the code
type InternalError struct {
Path string
}
func (e *InternalError) Error(a) string {
return fmt.Sprintf("parse %v: internal error", e.Path)
}
Copy the code
The interface contains a method Error() that returns an Error message as a string. Each type that implements the wrong interface can be used as an error. Golang automatically calls the Error() method when errors are printed using methods such as fmt.println.
There are several ways to create custom error messages in Golang, each with its own advantages and disadvantages.
String based error
String-based errors can be customized using two out-of-the-box methods in Golang for relatively simple errors that return only descriptive error information.
err := errors.New("math: divided by zero")
Copy the code
Passing error information to the errors.new () method can be used to create a New error
err2 := fmt.Errorf("math: %g cannot be divided by zero", x)
Copy the code
FMT.Errorf You can add error information to your error message in a string format. Add some formatting for error messages.
Error customizing data structure
You can create custom Error types on your structure by implementing the Error() function defined in the Error interface. Here’s an example.
Defer, panic and recover
Go doesn’t have exceptions like many other programming languages (including Java and Javascript), but there is a similar mechanism called “Defer, Panic, and Recover “. However, the use of Panic and Recover is very different from exceptions in other programming languages, because the code itself cannot handle the time and unrecoverable situations.
Defer
It’s a bit like destructors, where you do some resource freefall after the function has finished executing. It doesn’t matter where it is in the code, so you can write it after your read/write resource statements so you don’t forget to do some resource freefall later on. The output of defer is also a question interviewers like to ask during the interview.
package main
import(
"fmt"
"os"
)
func main(a){
f := createFile("tmp/machinelearning.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
iferr ! =nil{
panic(err)
}
return f
}
func closeFile(f *os.File){
fmt.Println("closing")
err := f.Close()
iferr ! =nil{
fmt.Fprintf(os.Stderr, "error:%v\n",err)
os.Exit(1)}}func writeFile(f *os.File){
fmt.Println("writing")
fmt.Fprintln(f,"machine leanring")}Copy the code
The defer statement pushes the function into a stack structure. Meanwhile, functions in the stack structure are called after the return statement is executed.
package main
import "fmt"
func main(a){
// defer fmt.Println("word")
// fmt.Println("hello")
fmt.Println("hello")
for i := 0; i <=3; i++ {
defer fmt.Println(i)
}
fmt.Println("world")}Copy the code
hello
world
3
2
1
0
Copy the code
You can implement custom Error types on your structure by implementing the Error() function defined in the Error interface, as shown in the following example.
Panic
The panic statement signals to Golang that the code is usually unable to resolve the current problem, so it stops the normal flow of code execution. Once Panic is called, all delay functions are executed and the program crashes, with log information including panic values (usually error messages) and stack traces.
For example, panic occurs in Golang when a number is divided by 0.
package main
import "fmt"
func main(a){
divide(5)}func divide(x int){
fmt.Printf("divide(%d)\n",x+0/x)
divide(x- 1)}Copy the code
divide(5)
divide(4)
divide(3)
divide(2)
divide(1)
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
/Users/zidea2020/Desktop/mysite/go_tut/main.go:10 +0xdb
main.divide(0x1)
/Users/zidea2020/Desktop/mysite/go_tut/main.go:11 +0xcc
main.divide(0x2)
/Users/zidea2020/Desktop/mysite/go_tut/main.go:11 +0xcc
main.divide(0x3)
/Users/zidea2020/Desktop/mysite/go_tut/main.go:11 +0xcc
main.divide(0x4)
/Users/zidea2020/Desktop/mysite/go_tut/main.go:11 +0xcc
main.divide(0x5)
/Users/zidea2020/Desktop/mysite/go_tut/main.go:11 +0xcc
main.main()
/Users/zidea2020/Desktop/mysite/go_tut/main.go:6 +0x2a
exit status 2
Copy the code
Recover
The Go language provides the recover built-in function. As mentioned above, once panic occurs, the logic will Go to defer, so we will wait there. Calling the Recover function will catch the current panic, and the caught panic will not be passed up. The recovery will then end the current Panic state and return the error value of Panic.
package main
import "fmt"
func main(a){
accessSlice([]int{1.2.5.6.7.8}, 0)}func accessSlice(slice []int, index int) {
defer func(a) {
if p := recover(a); p ! =nil {
fmt.Printf("internal error: %v", p)
}
}()
fmt.Printf("item %d, value %d \n", index, slice[index])
defer fmt.Printf("defer %d \n", index)
accessSlice(slice, index+1)}Copy the code
Packaging error
Golang also allows errors to be wrapped in error nesting, adding additional information on top of the original error message to help the caller determine the problem and what to do with the information later. Save the original error by using the %w flag and the FMT.Errorf function to provide some specific information, as shown in the following example.
package main
import (
"errors"
"fmt"
"os"
)
func main(a) {
err := openFile("non-existing")
iferr ! =nil {
fmt.Printf("error running program: %s \n", err.Error())
}
}
func openFile(filename string) error {
if_, err := os.Open(filename); err ! =nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
Copy the code
The code above demonstrates how to wrap an error. The program prints the wrapped error with the filename added using FMT.Errorf, as well as the original error message passed to the %w flag. To add to this, Golang also provides the ability to retrieve the original error message by using error.unwrap to restore the error message.
package main
import (
"errors"
"fmt"
"os"
)
func main(a) {
err := openFile("non-existing")
iferr ! =nil {
fmt.Printf("error running program: %s \n", err.Error())
// Unwrap error
unwrappedErr := errors.Unwrap(err)
fmt.Printf("unwrapped error: %v \n", unwrappedErr)
}
}
func openFile(filename string) error {
if_, err := os.Open(filename); err ! =nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
Copy the code
Bad type conversion
Sometimes you need to convert between different error types, sometimes you need to convert to add information about the error, or to express it in a different way. The errors.As function provides a simple and safe way to transform output by looking for matching error types in the error chain. If no match is found, this function returns false.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(a){
// Casting error
if _, err := os.Open("non-existing"); err ! =nil {
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Copy the code
Here, you try to convert the generic error type to OS.patherror so that you can access that specific error information, which is stored on the Path property in the structure.
Error type checking
Golang provides the errors.Is function to check whether the error type Is the specified error type. This function returns a Boolean value indicating whether it Is the specified error type.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(a){
// Check if error is a specific type
if _, err := os.Open("non-existing"); err ! =nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")}else {
fmt.Println(err)
}
}
}
Copy the code