Gin framework - custom error handling

Keywords: PHP JSON github less encoding

Summary

Many readers ask me for the Demo source code of the Gin framework Practice Series in the background. Let me explain again here. I updated the source code to GitHub at https://github.com/xinliangnote/Go

Starting today's article, why do you want to customize error handling? What is the default error handling method?

Well, let's talk about default error handling first.

The default error handling is errors.New("error information"), which is returned by a return value of the error type.

Take a simple example:

   

func hello(name string) (str string, err error) {    
        if name == "" {    
            err = errors.New("name Can not be empty")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 



When this method is called:

 

  var name = ""    
    str, err :=  hello(name)    
    if err != nil {    
        fmt.Println(err.Error())    
        return    
    }

 



This is the default error handling, which will be described in the following example.

This default error handling just gets a string of error messages.

However...

I also want information about when the error occurred, file name, method name, line number, and so on.

I also want to alarm when I get errors, such as SMS alarm, email alarm, wechat alarm, etc.

I also want the call to be less complex, similar to the default error handling, such as:

alarm.WeChat("error message")
    return

In this way, we can get the information we want (time, file name, method name, line number), and alert us through wechat.

In the same way, the information we get from alarm.Email("error message") and alarm.Sms("error message") is the same, but the alarm mode is different.

We also need to ensure that in our business logic, when we get errors, we only get error information.

What we have come up with above is what we are going to implement today. Before we implement custom error handling, let's talk about Go error handling.

error handling

 

  package main    
    import (    
        "errors"    
        "fmt"    
    )    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            err = errors.New("name Can not be empty")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }    
    func main() {    
        var name = ""    
        fmt.Println("param:", name)    
        str, err := hello(name)    
        if err != nil {    
            fmt.Println(err.Error())    
            return    
        }    
        fmt.Println(str)    
    }

 



Output:

    param: Tom    
    hello: Tom

 



When name = '', output:

 

  param:    
    name Can not be empty

 


It is recommended that every function has error handling, and error should be the last return value.

Let's take a look at the official errors.go

 

  // Copyright 2011 The Go Authors. All rights reserved.    
    // Use of this source code is governed by a BSD-style    
    // license that can be found in the LICENSE file.    
    // Package errors implements functions to manipulate errors.    
    package errors    
    // New returns an error that formats as the given text.    
    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 e.s    
    }

 



The above code is not complicated. Refer to the above, let's write a custom error handling.

Custom error handling

Let's define an alarm.go to handle the alarm.

No more nonsense, just look at the code.

  

 package alarm    
    import (    
        "encoding/json"    
        "fmt"    
        "ginDemo/common/function"    
        "path/filepath"    
        "runtime"    
        "strings"    
    )    
    type errorString struct {    
        s string    
    }    
    type errorInfo struct {    
        Time     string `json:"time"`    
        Alarm    string `json:"alarm"`    
        Message  string `json:"message"`    
        Filename string `json:"filename"`    
        Line     int    `json:"line"`    
        Funcname string `json:"funcname"`    
    }    
    func (e *errorString) Error() string {    
        return e.s    
    }    
    func New (text string) error {    
        alarm("INFO", text)    
        return &errorString{text}    
    }    
    // Send emails    
    func Email (text string) error {    
        alarm("EMAIL", text)    
        return &errorString{text}    
    }    
    // Send message    
    func Sms (text string) error {    
        alarm("SMS", text)    
        return &errorString{text}    
    }    
    // Micro letter    
    func WeChat (text string) error {    
        alarm("WX", text)    
        return &errorString{text}    
    }    
    // Alarm method    
    func  alarm(level string, str string) {    
        // current time    
        currentTime := function.GetTimeStr()    
        // Define file name, line number, method name    
        fileName, line, functionName := "?", 0 , "?"    
        pc, fileName, line, ok := runtime.Caller(2)    
        if ok {    
            functionName = runtime.FuncForPC(pc).Name()    
            functionName = filepath.Ext(functionName)    
            functionName = strings.TrimPrefix(functionName, ".")    
        }    
        var msg = errorInfo {    
            Time     : currentTime,    
            Alarm    : level,    
            Message  : str,    
            Filename : fileName,    
            Line     : line,    
            Funcname : functionName,    
        }    
        jsons, errs := json.Marshal(msg)    
        if errs != nil {    
            fmt.Println("json marshal error:", errs)    
        }    
        errorJsonInfo := string(jsons)    
        fmt.Println(errorJsonInfo)    
        if level == "EMAIL" {    
            // Email execution    
        } else if level == "SMS" {    
            // Execute SMS    
        } else if level == "WX" {    
            // Execute wechat    
        } else if level == "INFO" {    
            // Execution logging    
        }    
    }

 



See how to call:

  

 package v1    
    import (    
        "fmt"    
        "ginDemo/common/alarm"    
        "ginDemo/entity"    
        "github.com/gin-gonic/gin"    
        "net/http"    
    )    
    func AddProduct(c *gin.Context)  {    
        // Get get parameter    
        name := c.Query("name")    
        var res = entity.Result{}    
        str, err := hello(name)    
        if err != nil {    
            res.SetCode(entity.CODE_ERROR)    
            res.SetMessage(err.Error())    
            c.JSON(http.StatusOK, res)    
            c.Abort()    
            return    
        }    
        res.SetCode(entity.CODE_SUCCESS)    
        res.SetMessage(str)    
        c.JSON(http.StatusOK, res)    
    }    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            err = alarm.WeChat("name Can not be empty")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 

 

Visit: http://localhost:8080/v1/product/add?name=a

    {    
        "code": 1,    
        "msg": "hello: a",    
        "data": null    
    }

 


No error is thrown, no information will be output.

Visit: http://localhost:8080/v1/product/add

    {    
        "code": -1,    
        "msg": "name cannot be empty",    
        "data": null    
    }

 


An error was thrown and the output information is as follows:

{"time":"2019-07-23 22:19:17","alarm":"WX","message":"name cannot be empty", "filename": "absolute path / ginDemo/router/v1/product.go","line":33,"funcname":"hello"}

Maybe some students will say, "bind the incoming parameters with the data binding and verification shared in the previous article:" required "can also be implemented.".

I can only say: "classmate, you don't understand my good intentions. This is just an example. You can use user-defined error handling in some complex business logic judgment scenarios.".

Here, we received the time, error message, file name, line number and method name when the error was reported.

It's easy to call.

Although the alarm mode is marked, there is no alarm notification.

I'd like to say that if you store data in the queue, and then perform asynchronous tasks to consume it, this will not be realized. You can improve it.

runtime.Caller() is used to read file name, method name and line number.

We also know that Go has panic and recover. What are they for? Let's talk about them next.

panic and recover

When the program cannot continue to run, you should use panic to throw errors.

When a panic occurs in a program, recover can be called inside defer (delay function) for control. However, there is a precondition that only in the same Go process can recover.

There are two kinds of panic, one is intentionally thrown out, the other is caused by careless program writing, let's say one by one.

Intentionally thrown panic:

  

 package main    
    import (    
        "fmt"    
    )    
    func main() {    
        fmt.Println("-- 1 --")    
        defer func() {    
            if r := recover(); r != nil {    
                fmt.Printf("panic: %s\n", r)    
            }    
            fmt.Println("-- 2 --")    
        }()    
        panic("i am panic")    
    }

 



Output:

 

   -- 1 --    
    panic: i am panic    
    -- 2 --

 


Unintentionally thrown panic:

  

 package main    
    import (    
        "fmt"    
    )    
    func main() {    
        fmt.Println("-- 1 --")    
        defer func() {    
            if r := recover(); r != nil {    
                fmt.Printf("panic: %s\n", r)    
            }    
            fmt.Println("-- 2 --")    
        }()    
        var slice = [] int {1, 2, 3, 4, 5}    
        slice[6] = 6    
    }

 


Output:

 

  -- 1 --    
    panic: runtime error: index out of range    
    -- 2 --

 



Both of the above are captured through recover, so how can we use them in the Gin framework? If you want to alarm when you receive the panic, how can you do it?

Since you want to implement the alarm, first define a Panic() method in ararm.go. When the panic exception occurs in the project, call this method, and the alarm will be implemented.

 

   // Panic anomaly    
    func Panic (text string) error {    
        alarm("PANIC", text)    
        return &errorString{text}    
    }

 


So how do we capture it?

Use middleware to capture and write a recover y middleware.

    package recover    
    import (    
        "fmt"    
        "ginDemo/common/alarm"    
        "github.com/gin-gonic/gin"    
    )    
    func Recover()  gin.HandlerFunc {    
        return func(c *gin.Context) {    
            defer func() {    
                if r := recover(); r != nil {    
                    alarm.Panic(fmt.Sprintf("%s", r))    
                }    
            }()    
            c.Next()    
        }    
    }

 



Route call middleware:

    r.Use(logger.LoggerToFile(), recover.Recover())    
    //Use can deliver multiple middleware.

 



Check it out. Let's throw two exceptions first to see if we can catch them?

Change the file product.go.

Intentionally throw panic:

   

package v1    
    import (    
        "fmt"    
        "ginDemo/entity"    
        "github.com/gin-gonic/gin"    
        "net/http"    
    )    
    func AddProduct(c *gin.Context)  {    
        // Get get parameter    
        name := c.Query("name")    
        var res = entity.Result{}    
        str, err := hello(name)    
        if err != nil {    
            res.SetCode(entity.CODE_ERROR)    
            res.SetMessage(err.Error())    
            c.JSON(http.StatusOK, res)    
            c.Abort()    
            return    
        }    
        res.SetCode(entity.CODE_SUCCESS)    
        res.SetMessage(str)    
        c.JSON(http.StatusOK, res)    
    }    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            // Intentionally throw panic    
            panic("i am panic")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 



Visit: http://localhost:8080/v1/product/add

The interface is blank.

An exception was thrown, and the output information is as follows:

{"time":"2019-07-23 22:42:37","alarm":"PANIC","message":"i am panic","filename": "absolute path / ginDemo/middleware/recover/recover.go","line":13,"funcname":"1"}

Obviously, the file name, method name and line number of the location are not what we want.

You need to adjust runtime.Caller(2), which is in the alarm method of alarm.go.

Adjust 2 to 4 and look at the output:

{"time":"2019-07-23 22:45:24","alarm":"PANIC","message":"i am panic","filename": "absolute path / ginDemo/router/v1/product.go","line":33,"funcname":"hello"}

That's right.

Unintentionally thrown panic:

 

   // The above code does not change    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            // Unintentionally throw panic    
            var slice = [] int {1, 2, 3, 4, 5}    
            slice[6] = 6    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 



Visit: http://localhost:8080/v1/product/add

The interface is blank.

An exception was thrown, and the output information is as follows:

{"time":"2019-07-23 22:50:06","alarm":"PANIC","message":"runtime error: index out of range","filename": "absolute path / runtime/panic.go","line":44,"funcname":"panicindex"}

Obviously, the file name, method name and line number of the location are not what we want.

Adjust 4 to 5 and see the output:

{"time":"2019-07-23 22:55:27","alarm":"PANIC","message":"runtime error: index out of range","filename": "absolute path / ginDemo/router/v1/product.go","line":34,"funcname":"hello"}

That's right.

Strange, why is that?

Here, it is necessary to talk about runtime.Caller(skip).

skip refers to the depth of the call to.

At 0, print the current call file and number of rows.

When it is 1, the number of files and lines called by the superior will be printed.

And so on

In this section, I need to pay attention to the call. I don't have a good solution yet.

I'm passing skip (call depth) as a parameter.

For example:

    // Micro letter    
    func WeChat (text string) error {    
        alarm("WX", text, 2)    
        return &errorString{text}    
    }    
    // Panic anomaly    
    func Panic (text string) error {    
        alarm("PANIC", text, 5)    
        return &errorString{text}    
    }

 



The specific code will not be pasted.

However, the call depth of intentionally throwing Panic is different from that of unintentionally throwing Panic. What should I do?

1. Try to change the Panic thrown intentionally to the way of throwing errors.

2. Find other ways to deal with it.


Posted by my_mind on Thu, 14 Nov 2019 06:45:23 -0800