Original text: hackernoon.com/golang-hand…
Note: Error in the translation can be understood as an exception, but there is a big difference between the error in Go and the exception in Java, which requires readers to slowly understand, so for the convenience of reading and thinking, the noun error in the translation is not translated.
The body of the
Go has a simple error handling model, but it’s not as simple as it looks. In this article, I will provide a good way to handle error and use this method to solve similar problems in future programming.
First, we’ll analyze the error in Go.
Then we will look at the generation and processing of error, and then analyze the defects.
Finally, we’ll explore a way to solve a similar problem we encountered in our programs.
What is error?
Looking at the definition of Error in the built-in package, we can draw some conclusions:
// The error type is defined in the built-in package as a simple interface
// where nil means no exception
type error interface {
Error() string
}
Copy the code
From the above code, we can see that error is an interface with only one error method.
Then we need to implement error is very simple, look at the following code:
type MyCustomError string
func (err MyCustomError) Error(a) string {
return string(err)
}
Copy the code
We use the standard packages FMT and errors to declare some errors:
import (
"errors"
"fmt"
)
simpleError := errors.New("a simple error")
simpleError2 := fmt.Errorf("an error from a %s string"."formatted")
Copy the code
Consider: Is this simple information enough to handle exceptions in the error definition above? Let’s go ahead and find a good solution.
The error processing flow
Now that we know what error looks like in Go, let’s take a look at the error handling process.
To follow the principles of parsimony and DRY (avoid duplicate code), error handling should only be done in one place.
Let’s look at the following example:
// Both handle error and return error
// This is a bad way to write
func someFunc(a) (Result, error) {
result, err := repository.Find(id)
iferr ! =nil {
log.Errof(err)
return Result{}, err
}
return result, nil
}
Copy the code
What’s wrong with this code?
We print the error and then return it to the caller, which is equivalent to repeating the error twice.
There is a good chance that a colleague in your group will use this method, and when an error occurs, he will probably print the error again, and the duplicate log will appear in the system log.
Let’s first assume that the program has three layers, namely data layer, interaction layer and interface layer:
// The data layer uses a third-party ORM library
func getFromRepository(id int) (Result, error) {
result := Result{ID: id}
err := orm.entity(&result)
iferr ! =nil {
return Result{}, err
}
return result, nil
}
Copy the code
According to the DRY principle, we can return the error to the uppermost interface layer of the call so that we can handle the error uniformly.
One problem with the code above, however, is that Go’s built-in error type has no call stack. Also, if the error is generated in a third-party library, we need to know which code in our project is responsible for the error.
github.com/pkg/errors can use this library to solve the above problems.
Using this library, I made some improvements to the code above, adding a call stack and some error messages.
import "github.com/pkg/errors"
// Use a third-party ORM library
func getFromRepository(id int) (Result, error) {
result := Result{ID: id}
err := orm.entity(&result)
iferr ! =nil {
return Result{}, errors.Wrapf(err, "error getting the result with id %d", id);
}
return result, nil
}
// When error is wrapped, the error message returned will look like this
// err.Error() -> error getting the result with id 10
// It is easy to know that this is an error from orM library
Copy the code
The above code encapsulates the ORM error, increasing the call stack, without modifying the original error information.
Then we’ll look at how this error is handled at other layers, starting with the interaction layer:
func getInteractor(idString string) (Result, error) {
id, err := strconv.Atoi(idString)
iferr ! =nil {
return Result{}, errors.Wrapf(err, "interactor converting id to int")}return repository.getFromRepository(id)
}
Copy the code
Next comes the interface layer:
func ResultHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
result, err := interactor.getInteractor(vars["id"])
iferr ! =nil {
handleError(w, err)
}
fmt.Fprintf(w, result)
}
Copy the code
func handleError(w http.ResponseWriter, err error) {
// Return HTTO 500 error
w.WriteHeader(http.StatusIntervalServerError)
log.Errorf(err)
fmt.Fprintf(w, err.Error())
}
Copy the code
Now we only handle error at the top interface layer, which looks perfect, right? No, if your program frequently returns HTTP error code 500 and prints errors to a log, useless logs like Result Not Found can be annoying.
The solution
We discussed above that a string alone is not enough to handle an error. We also know that we can trace the origin of the error and the final processing logic by adding some additional information to the error.
So I’ve defined three purposes of error handling.
The three purposes of error handling
- Provide a clear and complete call stack
- Provide error context information if necessary
- Print the error to the log (for example, you can print it in the frame layer)
Let’s create an error type:
const(
NoType = ErrorType(iota)
BadRequest
NotFound
// You can add the error type you want
)
type ErrorType uint
type customError struct {
errorType ErrorType
originalError error
contextInfo map[string]string
}
// Returns customError with specific error information
func (error customError) Error(a) string {
return error.originalError.Error()
}
// Create a new customError
func (type ErrorType) New(msg string) error {
return customError{errorType: type, originalError: errors.New(msg)}
}
// Customize the error information for customError
func (type ErrorType) Newf(msg string, args ...interface{}) error {
err := fmt.Errof(msg, args...)
return customError{errorType: type, originalError: err}
}
// Encapsulate error
func (type ErrorType) Wrap(err error, msg string) error {
return type.Wrapf(err, msg)
}
// Encapsulate error and add formatting information
func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error {
newErr := errors.Wrapf(err, msg, args..)
return customError{errorType: errorType, originalError: newErr}
}
Copy the code
As you can see from the code above, we can create a new error type or encapsulate an existing one. But we’re missing two things. One is that we don’t know the exact type of error. Second, we don’t know how to add the following information to this error.
To solve the above problems, let’s also encapsulate the github.com/pkg/errors method.
// Create a NoType error
func New(msg string) error {
return customError{errorType: NoType, originalError: errors.New(msg)}
}
// Create a NoType error with formatting information
func Newf(msg string, args ...interface{}) error {
return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))}
}
// Encapsulate error with an extra string
func Wrap(err error, msg string) error {
return Wrapf(err, msg)
}
// Return the original error
func Cause(err error) error {
return errors.Cause(err)
}
// error adds formatting information
func Wrapf(err error, msg string, args ...interface{}) error {
wrappedError := errors.Wrapf(err, msg, args...)
if customErr, ok := err.(customError); ok {
return customError{
errorType: customErr.errorType,
originalError: wrappedError,
contextInfo: customErr.contextInfo,
}
}
return customError{errorType: NoType, originalError: wrappedError}
}
Copy the code
Next we add context information to error:
// AddErrorContext adds a context to an error
func AddErrorContext(err error, field, message string) error {
context := errorContext{Field: field, Message: message}
if customErr, ok := err.(customError); ok {
return customError{errorType: customErr.errorType, originalError: customErr.originalError, contextInfo: context}
}
return customError{errorType: NoType, originalError: err, contextInfo: context}
}
// GetErrorContext returns the error context
func GetErrorContext(err error) map[string]string {
emptyContext := errorContext{}
ifcustomErr, ok := err.(customError); ok || customErr.contextInfo ! = emptyContext {return map[string]string{"field": customErr.context.Field, "message": customErr.context.Message}
}
return nil
}
// GetType returns the error type
func GetType(err error) ErrorType {
if customErr, ok := err.(customError); ok {
return customErr.errorType
}
return NoType
}
Copy the code
Now apply the above method to the example we wrote at the beginning of this article:
import "github.com/our_user/our_project/errors"
// The repository uses an external depedency orm
func getFromRepository(id int) (Result, error) {
result := Result{ID: id}
err := orm.entity(&result)
iferr ! =nil {
msg := fmt.Sprintf("error getting the result with id %d", id)
switch err {
case orm.NoResult:
err = errors.Wrapf(err, msg);
default:
err = errors.NotFound(err, msg);
}
return Result{}, err
}
return result, nil
}
// after the error wraping the result will be
// err.Error() -> error getting the result with id 10: whatever it comes from the orm
Copy the code
func getInteractor(idString string) (Result, error) {
id, err := strconv.Atoi(idString)
iferr ! =nil {
err = errors.BadRequest.Wrapf(err, "interactor converting id to int")
err = errors.AddContext(err, "id"."wrong id format, should be an integer")
return Result{}, err
}
return repository.getFromRepository(id)
}
func ResultHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
result, err := interactor.getInteractor(vars["id"])
iferr ! =nil {
handleError(w, err)
}
fmt.Fprintf(w, result)
}
Copy the code
func handleError(w http.ResponseWriter, err error) {
var status int
errorType := errors.GetType(err)
switch errorType {
case BadRequest:
status = http.StatusBadRequest
case NotFound:
status = http.StatusNotFound
default:
status = http.StatusInternalServerError
}
w.WriteHeader(status)
if errorType == errors.NoType {
log.Errorf(err)
}
fmt.Fprintf(w,"error %s", err.Error())
errorContext := errors.GetContext(err)
iferrorContext ! =nil {
fmt.Printf(w, "context %v", errorContext)
}
}
Copy the code
With simple encapsulation, we know exactly what type of error an error is, and then we can handle it easily.
You can also run the code through, or use the Errors library above to write some demos to further your understanding.