This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

1 introduction

Error handling is a very important link in Web background development. When reconstructing SpringBoot background with Gin this time, IT was found that Golang’s error handling was a little worded in Web development. Setting the CTX error body repeatedly in the API layer (equivalent to SpringBoot’s Controller layer) is a bit tedious, so the first thought is to do global error handling. However, after checking some blogs on the Internet, I found that Gin does not have a unified global error handling specification. Here is a record of what I think is a better approach – middleware error handling, along with some points that need to be paid attention to.

2 Project Structure

Or create a simple Gin project with a login interface and a test interface. Session mechanism is used for login authentication. For details, you can see that my previous blog Gin uses session middleware to obtain the current login user – nuggets (juejin. Cn). The project structure of this demo is as follows: Errors records custom error structures, Service does specific login logic and returns errors to the router, and Middleware handles specific errors. See Github for the complete code

| - gin_err_handler | | - errors | | -- errors. Go | | - middleware | | -- cookies. Go | | -- error. Go | | - model | | -- user. Go | | - service | | - user. Go | | -- go. Mod | | -- go. Sum | | -- main. GoCopy the code

3 Customize error structures

First, customize the following error structure, including code, MSG and data. Data is used to provide additional error information. For example, when a parameter error of 300 is displayed, data can prompt specific parameter error information. According to Golang’s interface implementation rules, the MyError structure is considered to implement the standard Error interface whenever it implements the Error() string method.

package errors

type MyError struct {
	Code int
	Msg  string
	Data interface{}}var (
	LOGIN_UNKNOWN = NewError(202."User does not exist")
	LOGIN_ERROR   = NewError(203."Wrong account or password")
	VALID_ERROR   = NewError(300."Parameter error")
	ERROR         = NewError(400."Operation failed")
	UNAUTHORIZED  = NewError(401."You are not logged in.")
	NOT_FOUND     = NewError(404."Resources do not exist")
	INNER_ERROR   = NewError(500."System exception"))func (e *MyError) Error(a) string {
	return e.Msg
}

func NewError(code int, msg string) *MyError {
	return &MyError{
		Msg:  msg,
		Code: code,
	}
}

func GetError(e *MyError, data interface{}) *MyError {
	return &MyError{
		Msg:  e.Msg,
		Code: e.Code,
		Data: data,
	}
}
Copy the code

4 Error Handling suggestions in Gin source code

Looking at Gin’s source code, you can see that the context.go structure provides an Errors field to pass Errors, and the authors also recommend in the Error() method comment that this method be used to store Errors and call middleware to handle them.

type Context struct {
        / /...
	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs
        / /...
}

/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/********* ERROR MANAGEMENT *********/
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

// Error attaches an error to the current context. The error is pushed to a list of errors.
// It's a good idea to call Error for each error that occurred during the resolution of a request.
// A middleware can be used to collect all the errors and push them to a database together,
// print a log, or append it in the HTTP response.
// Error will panic if err is nil.
func (c *Context) Error(err error) *Error {
	if err == nil {
		panic("err is nil")
	}

	parsedError, ok := err.(*Error)
	if! ok { parsedError = &Error{ Err: err, Type: ErrorTypePrivate, } } c.Errors =append(c.Errors, parsedError)
	return parsedError
}
Copy the code

5 Use intermediate processing errors

Knowing where the Context’s errors are stored, you can now write middleware to handle the errors. Note the processing flow of Gin middleware, as shown below. After the middleware calls the c.ext () method, it goes to the middleware behind the chain of execution. After the latter middleware is executed, it returns to the current middleware and executes the subsequent code. Therefore, if we want to use middleware to complete global error handling, we should put the error-handling middleware at the top of the execution chain, the position of middleware 1 in the figure; Also place the error-handling logic after c.next (), where code snippet 2 is.



Write the error handling middleware, the specific code is as follows, put C.ext () at the top, and then check if there is any error in C. Elors. If there is any custom error, put code, MSG and data in Response. If it is not a custom error, the server is displayed as an exception.

package middleware

import (
	"gin_err_handler/errors"
	"net/http"

	"github.com/gin-gonic/gin"
)

func ErrorHandler(a) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next() // call c.ext () to execute the middleware
		// Start from here after all middleware and router processing is complete
		// Check for errors in c. elors
		for _, e := range c.Errors {
			err := e.Err
			// Return code and MSG if there is a custom error
			if myErr, ok := err.(*errors.MyError); ok {
				c.JSON(http.StatusOK, gin.H{
					"code": myErr.Code,
					"msg":  myErr.Msg,
					"data": myErr.Data,
				})
			} else {
				Err.error ()
				// For example, err is set when save session fails
				c.JSON(http.StatusOK, gin.H{
					"code": 500."msg":  "Server exception"."data": err.Error(),
				})
			}
			return // Check for an error}}}Copy the code

6 Other middleware Settings are incorrect

Typically, projects also include middleware for authentication. In this case, session is used for authentication. When the session middleware discovers that the request does not carry cookies or does not contain session information in the cookie, it should immediately return an error. In Gin, c.abort () is provided to break the chain of middleware execution, as shown in the figure below. When it reaches C.abort (), the current code segment 2 is immediately executed in the middle, and the subsequent middleware goes straight back to the middleware level above without execution.



The processing logic in the session middleware is as follows: Check whether the current user information is saved in the session (it will be saved after successful login); if not, call c.abort () to break the middleware execution chain, and call c.elror to set custom errors.One thing to noteAlthough Gin provides the c.abbortwitherror () method to integrate the two steps, there is a problem with calling directly and the content-Type of the response will not be JSON. Gin has no solution for this problem.

package middleware

import (
	"gin_err_handler/errors"

	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
)

func Cookie(a) gin.HandlerFunc {
	return func(c *gin.Context) {
		session := sessions.Default(c)
		if session.Get("currentUser") = =nil {
			c.Abort()
			c.Error(errors.UNAUTHORIZED)
			// Abbreviating c. error (errors.unauthorized) may cause problems
			// The content-type of response is not JSON
			return
		}
		c.Next()
	}
}
Copy the code

7 The configuration at the routing and Service layers is incorrect

The return value of service contains an error field, and a custom error is returned when an error is encountered in the service code.

// service/user.go
package service

import (
	"gin_err_handler/errors"
	"gin_err_handler/model"
)

type UserService struct{}

var db = model.User{
	Id:       1,
	Username: "Alice",
	Email:    "[email protected]",
	Password: "123456",}// Write user data to death in code

func (s *UserService) Login(login model.Login) (*model.User, error) {
	u := &model.User{}
	iflogin.Email ! = db.Email {return nil, errors.LOGIN_UNKNOWN // 202 The user does not exist
	}
	iflogin.Password ! = db.Password {return nil, errors.LOGIN_ERROR
	}
	*u = db
	u.Password = "" // Password is sensitive information is not returned
	return u, nil
}
Copy the code

Abort is not used because there is no handler. Call the service interface. If an error occurs, the setting is incorrect.

// main.go
func setPulicRouter(r *gin.Engine) {
	r.POST("/login".func(c *gin.Context) {
		var loginVo model.Login
		// All errors generated are placed in c. ror
		// The router returns an error message without calling c.son ()
		ife := c.ShouldBindJSON(&loginVo); e ! =nil {
			myErr := errors.VALID_ERROR
			myErr.Data = e.Error()
			c.Error(myErr)
			return
		}
		u, err := userService.Login(loginVo)
                // If there is a custom error, the setting is wrong
		iferr ! =nil {
			c.Error(err)
			return
		}
		session := sessions.Default(c)
		session.Set("currentUser", u)
		ife := session.Save(); e ! =nil {
			// Session save errors are also handled by the middleware, not custom errors
			c.Error(e)
			return
		}
		c.JSON(http.StatusOK, gin.H{"code": 200."msg": "Login successful"})})}Copy the code

8 Registering Middleware

When registering middleware in the main function, note the middleware execution order mentioned in 5 and 6. First, put the error-handling middleware before all the routing and other middleware. Public routes are placed before session middleware and private routes are registered last.

func main(a) {
	gob.Register(model.User{})
	r := gin.Default()
	r.Use(middleware.ErrorHandler()) // Error-handling middleware comes first
	store := cookie.NewStore([]byte("yoursecret"))
	r.Use(sessions.Sessions("GSESSIONID", store))
	// Public routes do not require cookie authentication, so register before releasing session middleware
	setPulicRouter(r)
	r.Use(middleware.Cookie())
	setPrivateRouter(r)
	r.Run(": 8080")}Copy the code

9 Test Run

See my complete project codeMaking the sample, the inputgo run main.goRun the demo program, using Postman for the test

If a private route is accessed without login, an error message is displayed indicating that the error handling process between middleware components is correct.


If you enter an incorrect email address, password, or parameter, an error message is displayed, indicating that the error handling process is correct.

10 summary

Gin provides global error handling, and the most critical thing to note when using middleware to handle errors is the order of execution of the middleware and the subsequent execution logic of c.next () and c.abort (). There are some minor issues with the Gin package c.abbortwitherror (). It is recommended that c.abort () and c.elror () be used separately.