本篇是mygin这个系列的最后一篇。如果想自己动手实现一个类似Gin的Web框架,建议从 开始,
总代码行数有效行数只有600多行

github源码

目的

  • 实现错误处理机制

panic简介

在实现错误处理机制之前,我们知道在Go 中,错误的处理方式依靠return返回,由调用者处理。如果是不可恢复的错误,可以手动抛出错误,当然在实际运行中,也会遇到不可处理的错误,比如说除数为0的时候painc 也会触发。终止当前的程序。

手动触发错误

//main.go
func main() {
  fmt.Println("before ....")
  panic("some err message")
  fmt.Println("after ....")
}
  • shell
~  go run cmd/main.go
before ....
panic: some err message

goroutine 1 [running]:
main.main()
        /var/www/gophp/cmd/main.go:12 +0x5f
exit status 2

程序发生错误

func main() {
  i := []int{0}
  j := 2
  fmt.Println(j / i[0])
}
  • shell
go run cmd/main.go
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.main()
        /var/www/gophp/cmd/main.go:13 +0x194
exit status 2

painc 的介绍到此为止,也就是发生了painc 错误,对程序来说就是比较严重的,就会中断,但是这个时候需要捕获到这个painc 错误,Go中没有try...Catch...又想捕获错误,这个时候就该defer 出场了。

defer简介

panic会导致程序被中止,但是在程序终止前,会先把当前协程上已经defer的任务,执行完成后再终止。效果类似于其他语言的try...catch

示例

//cmd/main.go

func main() {
  defer func() {
  fmt.Println("defer....")
  }()
  i := []int{0}
  j := 2
  fmt.Println(j / i[0])
}
  • shell
~ go run cmd/main.go
defer....
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.main()
        /var/www/gophp/cmd/main.go:16 +0x1d3
exit status 2

可以看到,在程序退出之前先执行了defer 中的函数。如果能在defer 中捕获painc 错误,那么就能实现其他语言的try...catch

recover函数

介绍之前看官方文档怎么定义的

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
翻译后...
recover 内置函数允许程序管理恐慌的 goroutine 的行为。在延迟函数(但不是它调用的任何函数)
中执行恢复调用会通过恢复正常执行来停止 panic 序列,并检索传递给 panic 调用的错误值。如果
在延迟函数之外调用 recover,则不会停止紧急序列。在这种情况下,或者当 goroutine 没有 
panic 时,或者如果提供给 panic 的参数为 nil,则 recover 返回 nil。因此,recover 的
返回值报告 goroutine 是否处于恐慌状态。

应用

  • cmd/main.go
func main() {
  defer func() {
    fmt.Println("defer....")
    if err := recover(); err != nil {
      fmt.Printf("recover 到错误信息:%s\n", err.(error).Error())
	  fmt.Println("recover success")
     }
  }()
  i := []int{0}
  j := 2
  fmt.Println(j / i[0])
  fmt.Println("after panic")
  • shell
~ go run cmd/main.go
defer....
recover 到错误信息:runtime error: integer divide by zero
recover success

可以看到已经捕获到错误信息了,程序正常结束。 after panic 没有打印,这是正确的,当panic 被触发时,程序的执行栈就到定义的defer 函数。就像在try代码块中发生了异常,执行栈来到 catch,接下来执行 catch 代码块中的代码。而在 main() 中打印了 recover success,说明程序已经恢复正常,继续往下执行直到结束。

mygin的错误处理

对一个 Web 框架而言,错误处理机制是必要的。如果发生了painc错误,应当返回错误信息或告诉对方失败了,而不至于什么都不返回,对调用方十分不友好。
例如我有如下的逻辑

package main

import (
	"fmt"
	"github.com/scott-pb/mygin"
	"net/http"
	"strconv"
)

func main() {

	r := mygin.Default()
	group := r.Group("/api")
	group.GET("/recovery/:index", func(c *mygin.Context) {
		index := c.Params.ByName("index")
		i, _ := strconv.ParseInt(index, 10, 10)
		s := []int{1, 3, 5, 7, 9}

		c.String(http.StatusOK, fmt.Sprintf("index:%d result:%d success!\n", i, s[i]))
	})

	err := r.Run(":8088")
	if err != nil {
		fmt.Println(err)
	}
}

根据调用方传递的index下标函数数组中的值,当传递的index超过数组下标时,就会发生painc错误,如果执行/api/recovery/10 时,就会发生错误。调用方什么返回都没有。
这个时候,就需要添加一个错误的处理机制,当错误发生时,向调用方返回Internal Server Error,且打印必要的错误信息,方便进行错误定位。

实现中间件 Recovery

新增文件 mygin/recovery.go,在这个文件中实现中间件 Recovery

package mygin

import (
	"fmt"
	"net/http"
)

// Recovery 发生错误时,恢复函数,且返回相应错误信息。
func Recovery() HandlerFunc {
	return func(c *Context) {
		// 使用defer延迟执行,以便在函数退出时进行recover
		defer func() {
			if err := recover(); err != nil {
				// 如果发生panic,打印错误信息并返回500 Internal Server Error响应
				fmt.Println(err.(error).Error())
				c.Writer.Write([]byte("Internal Server Error\n"))
				c.status = http.StatusInternalServerError
				c.Abort() // 终止后续中间件的执行
			}
		}()

		c.Next() // 调用下一个中间件或处理函数
	}
}

Recovery 方法很简单,使用defer挂载上错误恢复的函数,在这个函数中调用recover方法,捕获panic,打印错误信息,并且向调用方返回 Internal Server Error。

// Default 返回一个默认的引擎实例
func Default() *Engine {
	engine := New()

    //Logger Recovery 中间件
	engine.Use(Logger(), Recovery())

	// Group 保存 engine 的指针
	engine.RouterGroup.engine = engine

	return engine
}

接下来就是测试了

测试

  • shell
~ curl http://127.0.0.1:8088/api/recovery/1
index:1 result:3 success!
~ curl http://127.0.0.1:8088/api/recovery/10
Internal Server Error
  • 控制台输出
    控制台输出
    可以看到第一次请求返回200成功,第二次请求,先打印了错误信息,然后对应的返回500错误,且调用方接收到了Internal Server Error。