add day4-day5 doc & fix some comments

This commit is contained in:
gzdaijie
2019-09-01 21:37:50 +08:00
parent 0e26d5abc1
commit 313aec9304
14 changed files with 423 additions and 18 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ Gee 的设计与实现参考了Gin,这个教程可以快速入门:[Go Gin简
- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html)[Code - Github](day1-http-base)
- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html)[Code - Github](day2-context)
- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html)[Code - Github](day3-router)
- 第四天:分组控制(Group)[Code - Github](day4-group)
- 第五天:中间件(Middleware)[Code - Github](day5-middleware)
- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html)[Code - Github](day4-group)
- [第五天:中间件(Middleware)]((https://geektutu.com/post/gee-day5.html)[Code - Github](day5-middleware)
- 第六天:HTML模板(Template)[Code - Github](day6-template)
- 第七天:错误恢复(Panic Recover)[Code - Github](day7-panic-recover)
+1 -1
View File
@@ -19,7 +19,7 @@ type (
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all group
groups []*RouterGroup // store all groups
}
)
+16 -5
View File
@@ -1,7 +1,15 @@
package main
/*
(1) v1
(1) index
curl -i http://localhost:9999/index
HTTP/1.1 200 OK
Date: Sun, 01 Sep 2019 08:12:23 GMT
Content-Length: 19
Content-Type: text/html; charset=utf-8
<h1>Index Page</h1>
(2) v1
$ curl -i http://localhost:9999/v1/
HTTP/1.1 200 OK
Date: Mon, 12 Aug 2019 18:11:07 GMT
@@ -9,19 +17,19 @@ Content-Length: 18
Content-Type: text/html; charset=utf-8
<h1>Hello Gee</h1>
(2)
(3)
$ curl "http://localhost:9999/v1/hello?name=geektutu"
hello geektutu, you're at /v1/hello
(3)
(4)
$ curl "http://localhost:9999/v2/hello/geektutu"
hello geektutu, you're at /hello/geektutu
(4)
(5)
$ curl "http://localhost:9999/v2/login" -X POST -d 'username=geektutu&password=1234'
{"password":"1234","username":"geektutu"}
(5)
(6)
$ curl "http://localhost:9999/hello"
404 NOT FOUND: /hello
*/
@@ -34,6 +42,9 @@ import (
func main() {
r := gee.New()
r.GET("/index", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Index Page</h1>")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
+1 -1
View File
@@ -20,7 +20,7 @@ type (
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all group
groups []*RouterGroup // store all groups
}
)
+1 -1
View File
@@ -22,7 +22,7 @@ type (
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all group
groups []*RouterGroup // store all groups
htmlTemplates *template.Template // for html render
funcMap template.FuncMap // for html render
}
+1 -1
View File
@@ -22,7 +22,7 @@ type (
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all group
groups []*RouterGroup // store all groups
htmlTemplates *template.Template // for html render
funcMap template.FuncMap // for html render
}
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: Go语言动手写Web框架 - Gee第一天 http.Handler
date: 2019-08-12 00:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,从零设计一个Web框架。本文介绍了Go标准库 net/http 和 http.Handler 接口的使用,拦截所有的 HTTP 请求,交给Gee框架处理。
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了Go标准库 net/http 和 http.Handler 接口的使用,拦截所有的 HTTP 请求,交给Gee框架处理。
tags:
- Go
categories:
+1 -1
View File
@@ -1,7 +1,7 @@
---
title: Go语言动手写Web框架 - Gee第二天 上下文Context
date: 2019-08-19 00:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,从零设计一个Web框架。本文介绍了请求上下文(Context)的设计理念,封装了返回JSON/String/Data/HTML等类型响应的方法。
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了请求上下文(Context)的设计理念,封装了返回JSON/String/Data/HTML等类型响应的方法。
tags:
- Go
categories:
+7 -2
View File
@@ -1,7 +1,7 @@
---
title: Go语言动手写Web框架 - Gee第三天 路由Router
title: Go语言动手写Web框架 - Gee第三天 前缀树路由Router
date: 2019-08-28 00:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,从零设计一个Web框架。本文介绍了如何用 Trie 前缀树实现路由。支持简单的参数解析和通配符的场景。
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何用 Trie 前缀树实现路由 Route。支持简单的参数解析和通配符的场景。
tags:
- Go
categories:
@@ -15,6 +15,11 @@ image: post/gee-day3/trie_router.jpg
github: https://github.com/geektutu/7days-golang
---
本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第三篇。
- 使用 Tire 树实现动态路由(dynamic route)解析。
- 支持两种模式`:name``*filepath`**代码约150行**。
## Trie 树简介
之前,我们用了一个非常简单的`map`结构存储了路由表,使用`map`存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。那如果我们想支持类似于`/hello/:name`这样的动态路由怎么办呢?所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。例如`/hello/:name`,可以匹配`/hello/geektutu``hello/jack`等。
+164
View File
@@ -0,0 +1,164 @@
---
title: Go语言动手写Web框架 - Gee第四天 分组控制Group
date: 2019-09-01 15:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了分组控制(Group Control)的意义,以及嵌套分组路由的实现。
tags:
- Go
categories:
- 从零实现
keywords:
- Go语言
- 从零实现Web框架
- 动手写Web框架
- Group Control
image: post/gee-day4/group.jpg
github: https://github.com/geektutu/7days-golang
---
本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第四篇。
- 实现路由分组控制(Route Group Control)**代码约50行**
## 分组的意义
分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:
-`/post`开头的路由匿名可访问。
-`/admin`开头的路由需要鉴权。
-`/api`开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。
大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如`/post`是一个分组,`/post/a``/post/b`可以是该分组下的子分组。作用在`/post`分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。
中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如`/admin`的分组,可以应用鉴权中间件;`/`分组应用日志中间件,`/`是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。
提供扩展能力支持中间件的内容,我们将在下一节当中介绍。
##分组嵌套
一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如`/`,或者`/api`;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。还记得,我们之前调用函数`(*Engine).addRoute()`来映射所有的路由规则和 Handler 。如果Group对象需要直接映射路由规则的话,比如我们想在使用框架时,这么调用:
```go
r := gee.New()
v1 := r.Group("/v1")
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
```
那么Group对象,还需要有访问`Router`的能力,为了方便,我们可以在Group中,保存一个指针,指向`Engine`,整个框架的所有资源都是由`Engine`统一协调的,那么就可以通过`Engine`间接地访问各种接口了。
所以,最后的 Group 的定义是这样的:
**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day4-group/gee)**
```go
RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
```
我们还可以进一步地抽象,将`Engine`作为最顶层的分组,也就是说`Engine`拥有`RouterGroup`所有的能力。
```go
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all groups
}
```
那我们就可以将和路由有关的函数,都交给`RouterGroup`实现了。
```go
// New is the constructor of gee.Engine
func New() *Engine {
engine := &Engine{router: newRouter()}
engine.RouterGroup = &RouterGroup{engine: engine}
engine.groups = []*RouterGroup{engine.RouterGroup}
return engine
}
// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
engine := group.engine
newGroup := &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: engine,
}
engine.groups = append(engine.groups, newGroup)
return newGroup
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
pattern := group.prefix + comp
group.engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
group.addRoute("POST", pattern, handler)
}
```
可以仔细观察下`addRoute`函数,调用了`group.engine.router.addRoute`来实现了路由的映射。由于`Engine`从某种意义上继承了`RouterGroup`的所有属性和方法,因为 (*Engine).engine 是指向自己的。这样实现,我们既可以像原来一样添加路由,也可以通过分组添加路由。
## 使用 Demo
测试框架的Demo就可以这样写了:
```go
func main() {
r := gee.New()
r.GET("/index", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Index Page</h1>")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v1.GET("/hello", func(c *gee.Context) {
// expect /hello?name=geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
}
v2 := r.Group("/v2")
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
v2.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
}
r.Run(":9999")
}
```
通过 curl 简单测试:
```bash
$ curl "http://localhost:9999/v1/hello?name=geektutu"
hello geektutu, you're at /v1/hello
$ curl "http://localhost:9999/v2/hello/geektutu"
hello geektutu, you're at /hello/geektutu
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+225
View File
@@ -0,0 +1,225 @@
---
title: Go语言动手写Web框架 - Gee第四天 中间件Middleware
date: 2019-09-01 20:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架添加中间件的功能(middlewares)。
tags:
- Go
categories:
- 从零实现
keywords:
- Go语言
- 从零实现Web框架
- 动手写Web框架
- Middlewares
image: post/gee-day5/middleware.jpg
github: https://github.com/geektutu/7days-golang
---
本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第五篇。
- 设计并实现 Web 框架的中间件(Middlewares)机制。
- 实现通用的`Logger`中间件,能够记录请求到响应所花费的时间,**代码约50行**
## 中间件是什么
中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:
- 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
- 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。
那对于一个 Web 框架而言,中间件应该设计成什么样呢?接下来的实现,基本参考了 Gin 框架。
## 中间件设计
Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是`Context`对象。插入点是框架接收到请求初始化`Context`对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对`Context`进行二次加工。另外通过调用`(*Context).Next()`函数,中间件可等待用户自己定义的 `Handler`处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,`c.Next()`表示等待执行其他的中间件或用户的`Handler`
****[day4-group/gee/logger.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)****
```go
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
```
另外,支持设置多个中间件,依次进行调用。
我们上一篇文章[分组控制 Group Control](https://geektutu.com/post/gee-day4.html)中讲到,中间件是应用在`RouterGroup`上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。那为什么不作用在每一条路由规则上呢?作用在某条路由规则,那还不如用户直接在 Handler 中调用直观。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。
我们之前的框架设计是这样的,当接收到请求后,匹配路由,该请求的所有信息都保存在`Context`中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在`Context`中,依次进行调用。为什么依次调用后,还需要在`Context`中保存呢?因为在设计中,中间件不仅作用在处理流程前,也可以作用在处理流程后,即在用户定义的 Handler 处理完毕后,还可以执行剩下的操作。
为此,我们给`Context`添加了2个参数,定义了`Next`方法:
**[day4-group/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)**
```go
type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
Params map[string]string
// response info
StatusCode int
// middleware
handlers []HandlerFunc
index int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Path: req.URL.Path,
Method: req.Method,
Req: req,
Writer: w,
index: -1,
}
}
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
```
`index`是记录当前执行到第几个中间件,当在中间件中调用`Next`方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在`Next`方法之后定义的部分。如果我们将用户在映射路由时定义的`Handler`添加到`c.handlers`列表中,结果会怎么样呢?想必你已经猜到了。
```
func A(c *Context) {
part1
c.Next()
part2
}
func B(c *Context) {
part3
c.Next()
part4
}
```
假设我们应用了中间件 A 和 B,和路由映射的 Handler。`c.handlers`是这样的[A, B, Handler]`c.index`初始化为-1。调用`c.Next()`,接下来的流程是这样的:
- c.index++c.index 变为 0
- 0 < 3,调用 c.handlers[0],即 A
- 执行 part1,调用 c.Next()
- c.index++c.index 变为 1
- 1 < 3,调用 c.handlers[1],即 B
- 执行 part3,调用 c.Next()
- c.index++c.index 变为 2
- 2 < 3,调用 c.handlers[2],即Handler
- Handler 调用完毕,返回到 B 中的 part4,执行 part4
- part4 执行完毕,返回到 A 中的 part2,执行 part2
- part2 执行完毕,结束。
一句话说清楚重点,最终的顺序是`part1 -> part3 -> Handler -> part 4 -> part2`。恰恰满足了我们对中间件的要求,接下来看调用部分的代码,就能全部串起来了。
## 代码实现
- 定义`Use`函数,将中间件应用到某个 Group 。
**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)**
```go
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
engine.router.handle(c)
}
```
ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 `c.handlers`
- handle 函数中,将从路由匹配得到的 Handler 添加到 `c.handlers`列表中,执行`c.Next()`
**[day4-group/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)**
```go
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
key := c.Method + "-" + n.pattern
c.Params = params
c.handlers = append(c.handlers, r.handlers[key])
} else {
c.handlers = append(c.handlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
})
}
c.Next()
}
```
## 使用 Demo
```go
func onlyForV2() gee.HandlerFunc {
return func(c *gee.Context) {
// Start timer
t := time.Now()
// if a server error occurred
c.Fail(500, "Internal Server Error")
// Calculate resolution time
log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
func main() {
r := gee.New()
r.Use(gee.Logger()) // global midlleware
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v2 := r.Group("/v2")
v2.Use(onlyForV2()) // v2 group middleware
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
r.Run(":9999")
}
```
`gee.Logger()`即我们一开始就介绍的中间件,我们将这个中间件和框架代码放在了一起,作为框架默认提供的中间件。在这个例子中,我们将`gee.Logger()`应用在了全局,所有的路由都会应用该中间件。`onlyForV2()`是用来测试功能的,仅在`v2`对应的 Group 中应用了。
接下来使用 curl 测试,可以看到,v2 Group 2个中间件都生效了。
```bash
$ curl http://localhost:9999/
>>> log
2019/08/17 01:37:38 [200] / in 3.14µs
(2) global + group middleware
$ curl http://localhost:9999/v2/hello/geektutu
>>> log
2019/08/17 01:38:48 [200] /v2/hello/geektutu in 61.467µs for group v2
2019/08/17 01:38:48 [200] /v2/hello/geektutu in 281µs
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

+3 -3
View File
@@ -1,7 +1,7 @@
---
title: 7天用Go从零实现Web框架Gee教程
date: 2019-08-11 02:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,从零设计一个Web框架
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架
tags:
- Go
categories:
@@ -65,7 +65,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day1-http-base)
- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day2-context)
- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day3-router)
- 第四天:分组控制(Group)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day4-group)
- 第五天:中间件(Middleware)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day5-middleware)
- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day4-group)
- [第五天:中间件(Middleware)]((https://geektutu.com/post/gee-day5.html)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day5-middleware)
- 第六天:HTML模板(Template)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day6-template)
- 第七天:错误恢复(Panic Recover)[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover)