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.