error in Golang
error in Golang is a simple interface type. As long as this interface is implemented, it can be regarded as an error
type error interface { Error() string }
Several ways to play error
Looking through the Golang source code, you can see many error types similar to the following
Sentinel error
var EOF = ("EOF") var ErrUnexpectedEOF = ("unexpected EOF") var ErrNoProgress = ("multiple Read calls return no data or error")
shortcoming:
1. Make errors have a duality
error != nil no longer means that an error must have occurred
For example, to return to tell the caller that there is no more data, but this is not an error
2. Create a dependency between two packages
If you use it to check whether all the data has been read, then the io package will be imported into the code
Custom error type
A good example is that it has the advantage of having more context information attached
type PathError struct { Op string Path string Err error }
Wrap error
At this point we can find that Golang's error is very simple, but simplicity also means that sometimes it is not enough.
Golang's error has always had two problems:
No file: line information (that is, no stack information)
For example, this kind of error, if you know which line of the code is reported wrong, it is simply fatal when you debug it.
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
Error 1406: Data too long for column 'content' at row 1
2. When the upper layer error wants to attach more log information, it will often use it.()
,()
A new error will be created, and the underlying error type will be "swallowed"
var errNoRows = ("no rows") // Imitate the SQL library to return an errNoRowsfunc sqlExec() error { return errNoRows } func serviceNoErrWrap() error { err := sqlExec() if err != nil { return ("sqlExec :%v", err) } return nil } func TestErrWrap(t *) { // Created a new err with missing the underlying err err := serviceNoErrWrap() if err != errNoRows { ("===== errType don't equal errNoRows =====") } } -------------------------------Code running results---------------------------------- === RUN TestErrWrap 2022/03/26 17:19:43 ===== errType don't equal errNoRows =====
To solve this problem, we can use/pkg/error package
,use()method
Keep err
Save towithStack object
// The withStack structure saves an error, forming an error chain. At the same time, the *stack field saves the stack information.type withStack struct { error *stack }
Can also be used(err, "custom text")
, with some custom text information
Source code interpretation: first package err and messagewithMessage object
, thenwithMessage object
and stack information packagewithStack object
func Wrap(err error, message string) error { if err == nil { return nil } err = &withMessage{ cause: err, msg: message, } return &withStack{ err, callers(), } }
New features of Golang version 1.13 error
Golang 1.13 version borrowed from/pkg/error package
, the following functions have been added, greatly enhancing the ability of Golang language to judge error types
()
// Contrary to () behavior// Get the underlying err in the err chainfunc Unwrap(err error) error { u, ok := err.(interface { Unwrap() error }) if !ok { return nil } return () }
()
Before version 1.13, we can useerr == targetErr
Determine err type()
It's its enhanced version: error chaineither err == targetErr
,Right nowreturn true
// Practice: Learn to use()var errNoRows = ("no rows") // Imitate the SQL library to return an errNoRowsfunc sqlExec() error { return errNoRows } func service() error { err := sqlExec() if err != nil { return (err) // Package errNoRows } return nil } func TestErrIs(t *) { err := service() // Recursively call, hit any err on the err chain will return true if (err, errNoRows) { ("===== () succeeded =====") } // Err is packaged and cannot be judged by == if err == errNoRows { ("err == errNoRows") } } -------------------------------Code running results---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== () succeeded =====
Example interpretation:
Because of usePacked
sqlError
,sqlError
Located at the bottom of the error chain, the upper error is no longersqlError
Type, so use==
Can't tell the bottomsqlError
Source code interpretation:
- It's easy to think of its internal calls
err = Unwrap(err)
Method to get the error at the bottom of the error chain - Custom error types can be implemented
Is interface
Customize error type judgment method
func Is(err, target error) bool { if target == nil { return err == target } isComparable := (target).Comparable() for { if isComparable && err == target { return true } // Support custom error type judgment if x, ok := err.(interface{ Is(error) bool }); ok && (target) { return true } if err = Unwrap(err); err == nil { return false } } }
Let's take a look at how to customize error type judgment:
CustomerrNoRows type
, it must be implementedIs interface
, to use()
Make type judgment
type errNoRows struct { Desc string } func (e errNoRows) Unwrap() error { return e } func (e errNoRows) Error() string { return } func (e errNoRows) Is(err error) bool { return (err).Name() == (e).Name() } // Imitate the SQL library to return an errNoRowsfunc sqlExec() error { return &errNoRows{"Kaolengmian NB"} } func service() error { err := sqlExec() if err != nil { return (err) } return nil } func serviceNoErrWrap() error { err := sqlExec() if err != nil { return ("sqlExec :%v", err) } return nil } func TestErrIs(t *) { err := service() if (err, errNoRows{}) { ("===== () succeeded =====") } } -------------------------------Code running results---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== () succeeded =====
()
Before version 1.13, we can useif _,ok := err.(targetErr)
Determine err type()
It's its enhanced version: error chainAny err is the same as targetErr
,Right nowreturn true
// Learn to use() through examplestype sqlError struct { error } func (e *sqlError) IsNoRows() bool { t, ok := .(ErrNoRows) return ok && () } type ErrNoRows interface { IsNoRows() bool } // Return a sqlErrorfunc sqlExec() error { return sqlError{} } // Package sqlErrorfunc service() error { err := sqlExec() if err != nil { return (err) } return nil } func TestErrAs(t *) { err := service() // Use recursively, as long as there is an Err on the Err chain satisfying the type assertion, it returns true sr := &sqlError{} if (err, sr) { ("===== () succeeded =====") } // After packaging, the current Err cannot be converted to the underlying Err through type assertion if _, ok := err.(sqlError); ok { ("===== type assert succeeded =====") } } ----------------------------------Code running results-------------------------------------------- === RUN TestErrAs 2022/03/25 18:09:02 ===== () succeeded =====
Example interpretation:
Because of usePacked
sqlError
,sqlError
Located at the bottom of the error chain, the upper error is no longersqlError
Type, so using type assertions cannot determine the underlyingsqlError
error handling best practices
The above talks about how to define the error type and how to compare the error type. Now let's talk about how to do error processing in large projects.
Priority to error
When a function returns a non-empty error, the error should be processed first, and its other return values should be ignored
Only handle error once
- In Golang, for each err we should only handle it once.
- Either immediately handle err (including logging and other behaviors) and return nil (swallow the error). At this time, because the error has been downgraded, be careful to handle the function return value.
For example, the following example (conf) does not have return err, so be careful of errors such as null pointers when using buf.
Or return err, process err on the upper layer
Counterexample:
// Imagine if the writeAll function errors, the log will be printed twice// If the entire project does this, you will be surprised to find that we are logging everywhere, and there are a lot of valuable garbage logs in the project.// unable to write: // could not write config: type config struct {} func writeAll(w , buf []byte) error { _, err := (buf) if err != nil { ("unable to write:", err) return err } return nil } func writeConfig(w , conf *config) error { buf, err := (conf) if err != nil { ("could not marshal config:%v", err) } if err := writeAll(w, buf); err != nil { ("count not write config: %v", err) return err } return nil }
Don't wrap error repeatedly
We should package error, but only once
Upper level business code suggestionsWrap error
, but the underlying basic Kit library is not recommended
If the underlying basic Kit library is packaged once and the upper business code is packaged once again, the error is packaged repeatedly, and the log will be re-blown
For example, what we often usesql library
Will returnThis predefined error, instead of giving us a wrapped error
Opacity error handling
In large projects, it is recommended to useOpacity errors
: Don't care about the error type, just care whether the error is nil
benefit:
The coupling is small, so there is no need to judge a specific error type, so there is no need to import the dependencies of related packages.
However, sometimes, this way of handling error is not enough, such as: the business needs to be correctParameter exception error type
Degrading and printing Warn-level logs
type ParamInvalidError struct { Desc string } func (e ParamInvalidError) Unwrap() error { return e } func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + } func (e ParamInvalidError) Is(err error) bool { return (err).Name() == (e).Name() } func NewParamInvalidErr(desc string) error { return (&ParamInvalidError{Desc: desc}) } ------------------------------Top-level log printing--------------------------------- if (err, {}) { (ctx, "%s", ()) return } if err != nil { (ctx, " error:%+v", err) }
Simplify error handling
Golang because of countlessif err != nil
Criticized, now let's see how to reduce itif err != nil
This code
CountLines() implements the "number of rows to read content" function
The error can be simplified by using ():
func CountLines(r ) (int, error) { var ( br = (r) lines int err error ) for { _, err := ('\n') lines++ if err != nil { break } } if err != { return 0, nilsadwawa } return lines, nil } func CountLinesGracefulErr(r ) (int, error) { sc := (r) lines := 0 for () { lines++ } return lines, () }
()
Return oneScanner
Object, the structure contains the error type, and the callErr()
Method can return the encapsulated error
Golang source code contains a lot of excellent design ideas. We learn from it when reading the source code and use it in practice.
type Scanner struct { r // The reader provided by the client. split SplitFunc // The function to split the tokens. maxTokenSize int // Maximum size of a token; modified by tests. token []byte // Last token returned by split. buf []byte // Buffer used as argument to split. start int // First non-processed byte in buf. end int // End of data in buf. err error // Sticky error. empties int // Count of successive empty tokens. scanCalled bool // Scan has been called; buffer is in use. done bool // Scan has finished. } func (s *Scanner) Err() error { if == { return nil } return }
errWriter
WriteResponse()
The function is implemented"Build HttpResponse"
Function
Using the ideas learned above, we can implement one by ourselveserrWriter
Object, simplifies the processing of error
type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w , st Status, headers []Header, body ) error { _, err := (w, "HTTP/1.1 %d %s\r\n", , ) if err != nil { return err } for _, h := range headers { _, err := (w, "%s: %s\r\n", , ) if err != nil { return err } } if _, err := (w, "\r\n"); err != nil { return err } _, err = (w, body) return err } type errWriter struct { err error } func (e *errWriter) Write(buf []byte) (n int, err error) { if != nil { return 0, } n, = (buf) return n, nil } func WriteResponseGracefulErr(w , st Status, headers []Header, body ) error { ew := &errWriter{w, nil} (ew, "HTTP/1.1 %d %s\r\n", , ) for _, h := range headers { (ew, "%s: %s\r\n", , ) } (w, "\r\n") (ew, body) return }
When should I use panic
In Golangpanic
It will cause the program to exit directly, which is a fatal error.
It is recommended to use panic only when a fatal program error occurs, such as index out-of-bounds, irrecoverable environmental problems, stack overflow, etc.
Small addition
()
The return iserrorString object
The reason for pointer to prevent string collisions. If a collision occurs, the two error objects will be equal.
Source code:
func New(text string) error { return &errorString{text} } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return }
practice:error1
anderror2
All texts"error"
, but the two are not equal
func TestErrString(t *) { var error1 = ("error") var error2 = ("error") if error1 != error2 { ("error1 != error2") } } ---------------------Code running results-------------------------- === RUN TestXXXX 2022/03/25 22:05:40 error1 != error2
References
《Effective GO》
"Go Programming Language"
/practical-go/presentations/#_error_handling
Summarize
This is all about this article about error processing in Golang. For more information about error processing in Golang, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!