Go项目-Gee Web框架
Go项目-Gee Web框架
完成的功能
- 简单介绍
net/http
库以及http.Handler
接口 - 将
路由(router)
独立出来,方便之后增强。 - 设计
上下文(Context)
,封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。 - 使用 Trie 树实现动态路由(dynamic route)解析。
- 实现路由分组控制(Route Group Control)
- 设计并实现 Web 框架的中间件(Middlewares)机制。
- 实现通用的
Logger
中间件,能够记录请求到响应所花费的时间, - 实现静态资源服务(Static Resource)。
- 支持HTML模板渲染。
- 实现错误处理机制。
http.Handler
Go语言内置了 net/http
库,封装了HTTP网络编程的基础的接口,使用这个库:
package main
import (
"fmt"
"net/http"
)
func main() {
// 设置两个路由
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
// 启动Web服务,在9999端口进行监听,处理所有的HTTP请求的实例
http.ListenAndServe("localhost:9999", nil)
// 最后的nil即为实现框架的入口
}
// 根路由
func indexHandler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
}
// hello路由
func helloHandler(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
}
使用curl进行测试:
> curl http://localhost:9999/
URL.Path="/"
> curl http://localhost:9999/hello
Header["User-Agent"] = ["curl/7.68.0"]
Header["Accept"] = ["*/*"]
其中代码的nil就是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。
拦截一下请求进行尝试
package main
import (
"fmt"
"net/http"
)
// 定义一个空结构体,因为后面实现的是一个方法,比如在一个结构体的基础上进行实现
type Engine struct{}
// 实现ServeHTTP方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
case "/hello":
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
default:
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
func main() {
engine := &Engine{}
// 多设置一个路由
http.HandleFunc("/hi", indexHandler)
// 启动Web服务,在9999端口进行监听,处理所有的HTTP请求的实例
http.ListenAndServe("localhost:9999", engine)
// 最后的nil即为实现框架的入口
}
// 根路由
func indexHandler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
}
测试:
> curl http://localhost:9999/hello
Header["User-Agent"] = ["curl/7.68.0"]
Header["Accept"] = ["*/*"]
> curl http://localhost:9999/
URL.Path="/"
> curl http://localhost:9999/hi
404 NOT FOUND: /hi
因此就将所有的HTTP请求转向了自己的处理逻辑,代码的运行结果与之前的是一致的。
我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
因此就可以从这里入手完成这个Web框架,最终的代码结构是这样的
.
├── gee
│ └── gee.go
└── main.go
main.go:
使用 New()
创建 gee 的实例,使用 GET()
方法添加路由,最后使用 Run()
启动Web服务。
package main
import (
"Go-Projects/Gee/gee"
"fmt"
"net/http"
)
func main() {
r := gee.New()
r.Get("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path=%q\n", req.URL.Path)
})
r.Get("/hello", func(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})
r.Run("localhost:9999")
}
gee.go
package gee
import (
"fmt"
"net/http"
)
// 定义一个普遍使用的函数类型,避免后面再次定义
type HandlerFunc func(http.ResponseWriter, *http.Request)
// 定义路由表
type Engine struct {
router map[string]HandlerFunc
}
// 工厂模式的构造方法,返回一个实例
func New() *Engine {
return &Engine{
router: make(map[string]HandlerFunc),
}
}
// 将路由添加到路由表中
func (engine *Engine) addRoute(method, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
engine.router[key] = handler
}
// 实现GET方法
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
// 实现POST方法
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// 实现Run方法
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
// 完成统一的控制入口方法ServeHTTP
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
测试后的效果和之前完全相同。
整个 Gee
框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。
上下文Context
最终调用的效果:
package main
import (
"Go-Projects/Gee/gee"
"net/http"
)
func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.GET("/hello", func(c *gee.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
r.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
r.Run("localhost:9999")
}
Handler
的参数变成成了gee.Context
,提供了查询Query/PostForm参数的功能。gee.Context
封装了HTML/String/JSON
函数,能够快速构造HTTP响应。
- 对Web服务来说,无非是根据请求
*http.Request
,构造响应http.ResponseWriter
。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。 - 针对使用场景,封装
*http.Request
和http.ResponseWriter
的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name
,参数:name
的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。
context.go
package gee
import (
"encoding/json"
"fmt"
"net/http"
)
// 给map[string]interface{}起了一个别名gee.H,构建JSON数据时,显得更简洁。
type H map[string]interface{}
type Context struct {
// 原始的两个参数
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string
Method string
// 响应信息
StatusCode int
}
// 创建一个Context实例
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Writer: w,
Req: req,
Path: req.URL.Path,
Method: req.Method,
}
}
// 根据key返回用户输入的value,属于POST方法的工具
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}
// 根据key返回用户输入的value,属于GET方法的工具
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}
// 写入状态码并更改Context的状态码
func (c *Context) Status(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}
// 帮助下面的方法快速构造响应
func (c *Context) SetHeader(key, value string) {
c.Writer.Header().Set(key, value)
}
// 构造字符串类型的响应
func (c *Context) String(code int, format string, values ...interface{}) {
c.SetHeader("Content-Type", "text/plain")
c.Status(code)
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
// 构造JSON类型的响应
func (c *Context) JSON(code int, obj interface{}) {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
encoder := json.NewEncoder(c.Writer) // 流数据构造json
if err := encoder.Encode(obj); err != nil {
http.Error(c.Writer, err.Error(), 500)
}
}
// 构造data类型的响应
func (c *Context) Data(code int, data []byte) {
c.Status(code)
c.Writer.Write(data)
}
// 构造HTML类型的响应
func (c *Context) HTML(code int, html string) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
c.Writer.Write([]byte(html))
}
将和路由相关的方法和结构提取出来,放到了一个新的文件中 router.go
,方便我下一次对 router 的功能进行增强,
package gee
import (
"log"
"net/http"
)
type router struct {
handlers map[string]HandlerFunc
}
func newRouter() *router {
return &router{
handlers: make(map[string]HandlerFunc),
}
}
// 将路由添加到路由表中
func (r *router) addRoute(method, pattern string, handler HandlerFunc) {
log.Printf("Route %4s - %s", method, pattern)
key := method + "-" + pattern
r.handlers[key] = handler
}
// 路由处理
func (r *router) handle(c *Context) {
key := c.Method + "-" + c.Path
if handler, ok := r.handlers[key]; ok {
handler(c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
调整主框架入口gee.go
package gee
import (
"net/http"
)
// 定义一个普遍使用的函数类型,避免后面再次定义
type HandlerFunc func(*Context)
// 定义路由表
type Engine struct {
router *router
}
// 工厂模式的构造方法,返回一个实例
func New() *Engine {
return &Engine{
router: newRouter(),
}
}
// 实现GET方法
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.router.addRoute("GET", pattern, handler)
}
// 实现POST方法
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.router.addRoute("POST", pattern, handler)
}
// 实现Run方法
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
// 完成统一的控制入口方法ServeHTTP
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := newContext(w, req)
engine.router.handle(c)
}
测试代码
启动程序后:
2022/11/21 21:05:40 Route GET - /
2022/11/21 21:05:40 Route GET - /hello
2022/11/21 21:05:40 Route POST - /login
> curl -i http://localhost:9999/
HTTP/1.1 200 OK
Content-Type: text/html
Date: Mon, 21 Nov 2022 13:05:47 GMT
Content-Length: 19
<h1>Hello Gee</h1>
> curl "http://localhost:9999/hello?name=geektutu"
hello geektutu, you're at /hello
> curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
{"password":"1234","username":"geektutu"}
> curl "http://localhost:9999/xxx"
404 NOT FOUND: /xxx
前缀树路由
之前,我们用了一个非常简单的 map
结构存储了路由表,使用 map
存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。那如果我们想支持类似于 /hello/:name
这样的动态路由怎么办呢?
实现动态路由最常用的数据结构,被称为前缀树(Trie树),每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。
首先设计树节点上应该存储哪些信息
type node struct {
pattern string // 待匹配路由,例如 /p/:lang
part string // 路由中的一部分,例如 :lang
children []*node // 子节点,例如 [doc, tutorial, intro]
isWild bool // 是否精确匹配,part 含有 : 或 * 时为true
}
将匹配的逻辑,包装为一个辅助函数:
// 查找第一个匹配的节点,用于插入
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || n.isWild {
return child
}
}
return nil
}
// 查找全部匹配的节点,用于查找
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || n.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
实现节点的递归插入和查找
// 插入节点
func (n *node) insert(pattern string, parts []string, height int) {
// 到达高度了就停止
if len(parts) == height {
n.pattern = pattern
return
}
part := parts[height] // 获取当前的规则
child := n.matchChild(part) // 尝试用当前的规则进行匹配
// 如果没有匹配成功,就新建一个节点,并加入到当前节点的孩子们中去
if child == nil {
child = &node{
part: part,
isWild: part[0] == ':' || part[0] == '*',
}
n.children = append(n.children, child)
}
// 递归进行插入
child.insert(pattern, parts, height+1)
}
// 查询节点
func (n *node) search(parts []string, height int) *node {
if len(parts) == height || strings.HasPrefix(n.part, "*") {
if n.pattern == "" {
return nil
}
return n
}
part := parts[height] // 获取当前的规则
children := n.matchChildren(part) // 尝试用当前的规则进行匹配
// 遍历所有当前匹配的节点进行递归匹配
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
return result
}
}
return nil
}
使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中解析了 :
和 *
两种匹配符的参数,返回一个 map 。
// 将字符串解析成一个切片
func parsePattern(pattern string) []string {
vs := strings.Split(pattern, "/")
parts := make([]string, 0)
for _, item := range vs {
if item != "" {
parts = append(parts, item)
if item[0] == '*' {
break
}
}
}
return parts
}
// 将路由添加到路由表中
func (r *router) addRoute(method, pattern string, handler HandlerFunc) {
parts := parsePattern(pattern)
key := method + "-" + pattern
// 先看看是不是Get或者Post方法
_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}
r.roots[method].insert(pattern, parts, 0)
r.handlers[key] = handler
}
// 从路由表中查找路由
func (r *router) getRoute(method, path string) (*node, map[string]string) {
searchParts := parsePattern(path)
params := make(map[string]string)
root, ok := r.roots[method]
if !ok {
return nil, nil
}
n := root.search(searchParts, 0)
if n != nil {
parts := parsePattern(n.pattern)
for index, part := range parts {
if part[0] == ':' {
params[part[1:]] = searchParts[index]
}
if part[0] == '*' && len(part) > 1 {
params[part[1:]] = strings.Join(searchParts[index:], "/")
break
}
}
return n, params
}
return nil, nil
}
对 Context 对象增加一个属性和方法,来提供对路由参数的访问。我们将解析后的参数存储到 Params
中
更改路由处理的方法
// 路由处理
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
c.Params = params
key := c.Method + "-" + n.pattern
r.handlers[key](c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
测试:
> curl "http://localhost:9999/hello/geektutu"
hello geektutu, you're at /hello/geektutu
> curl "http://localhost:9999/assets/css/geektutu.css"
{"filepath":"css/geektutu.css"}
分组控制Group
真实的业务场景中,往往某一组路由需要相似的处理。例如:
- 以
/post
开头的路由匿名可访问。 - 以
/admin
开头的路由需要鉴权。 - 以
/api
开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。
一个 Group 对象需要具备哪些属性呢?
首先是前缀(prefix),比如 /
,或者 /api
;
要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;
中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。
还需要有访问 Router
的能力
// 分组路由
type RouterGroup struct {
prefix string
middlewares []HandlerFunc
parent *RouterGroup
engine *Engine
}
将 Engine
作为最顶层的分组,也就是说 Engine
拥有 RouterGroup
所有的能力。
// 扩展Engine
type Engine struct {
*RouterGroup
router *router
groups []*RouterGroup
}
更改下面的其他Engine方法即可
// 工厂模式的构造方法,返回一个实例
func New() *Engine {
engine := &Engine{
router: newRouter(),
}
engine.RouterGroup = &RouterGroup{
engine: engine,
}
engine.groups = []*RouterGroup{engine.RouterGroup}
return engine
}
增加一个Group的方法,创建一个新的RouterGroup
// 创建一个新的RouterGroup
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
}
后面的Get方法和Post方法就都换成RouterGroup的方法就可以了
测试:
> 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 /v2/hello/geektutu
> curl "http://localhost:9999/index"
<h1>Index Page</h1>
中间件Middleware
中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:
- 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
- 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。
Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是 Context
对象。插入点是框架接收到请求初始化 Context
对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context
进行二次加工。另外通过调用 (*Context).Next()
函数,中间件可等待用户自己定义的 Handler
处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()
表示等待执行其他的中间件或用户的 Handler
:
package gee
import (
"log"
"time"
)
func Logger() HandlerFunc {
return func(c *Context) {
t := time.Now() // 开始计时
c.Next() // 等待用户自己的Handler处理结束
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t)) // 打印时间
}
}
中间件是应用在 RouterGroup
上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。
给 Context
添加了2个参数,定义了 Next
方法:
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
index
是记录当前执行到第几个中间件,当在中间件中调用 Next
方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在 Next
方法之后定义的部分。
定义 Use
函数,将中间件应用到某个 Group 。
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers
。
// 完成统一的控制入口方法ServeHTTP
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)
}
handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers
列表中,执行 c.Next()
。
// 路由处理
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()
}
测试:
> go run Go-Projects/Gee
2022/11/22 15:45:00 Route GET - /
2022/11/22 15:45:00 Route GET - /v2/hello/:name
>
2022/11/22 15:45:11 [200] / in 3µs
>
2022/11/22 15:45:25 [500] /v2/hello/geektutu in 39.4µs for group v2
2022/11/22 15:45:25 [500] /v2/hello/geektutu in 77.6µs
> curl http://localhost:9999/
<h1>Hello Gee</h1>
> curl http://localhost:9999/v2/hello/geektutu
{"message":"Internal Server Error"}
模板(HTML Template)
Web 框架如何支持服务端渲染的场景
解析请求的地址,映射到服务器上文件的真实地址:
// 解析请求的地址,映射到服务器上文件的真实地址
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
absolutePath := path.Join(group.prefix, relativePath)
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) {
file := c.Param("filepath")
// Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil {
c.Status(http.StatusNotFound)
return
}
fileServer.ServeHTTP(c.Writer, c.Req)
}
}
func (group *RouterGroup) Static(relativePath string, root string) {
handler := group.createStaticHandler(relativePath, http.Dir(root))
urlPattern := path.Join(relativePath, "/*filepath")
// Register GET handlers
group.GET(urlPattern, handler)
}
HTML 模板渲染
// 扩展Engine
type Engine struct {
*RouterGroup
router *router
groups []*RouterGroup
htmlTemplates *template.Template // 模板渲染
funcMap template.FuncMap // 模板渲染
}
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.funcMap = funcMap
}
func (engine *Engine) LoadHTMLGlob(pattern string) {
engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))
}
对原来的 (*Context).HTML()
方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。
// 构造HTML类型的响应
func (c *Context) HTML(code int, name string, data interface{}) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
c.Fail(500, err.Error())
}
}
进行测试:
package main
import (
"Go-Projects/Gee/gee"
"fmt"
"html/template"
"net/http"
"time"
)
type student struct {
Name string
Age int8
}
func FormatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d-%02d-%02d", year, month, day)
}
func main() {
r := gee.New()
r.Use(gee.Logger())
r.SetFuncMap(template.FuncMap{
"FormatAsDate": FormatAsDate,
})
r.LoadHTMLGlob("Gee/templates/*")
r.Static("/assets", "./static")
stu1 := &student{Name: "Geektutu", Age: 20}
stu2 := &student{Name: "Jack", Age: 22}
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "css.tmpl", nil)
})
r.GET("/students", func(c *gee.Context) {
c.HTML(http.StatusOK, "arr.tmpl", gee.H{
"title": "gee",
"stuArr": [2]*student{stu1, stu2},
})
})
r.GET("/date", func(c *gee.Context) {
c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
"title": "gee",
"now": time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC),
})
})
r.Run("localhost:9999")
}
错误恢复(Panic Recover)
错误处理也可以作为一个中间件,增强 gee 框架的能力
package gee
import (
"fmt"
"log"
"net/http"
"runtime"
"strings"
)
// print stack trace for debug
func trace(message string) string {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:]) // skip first 3 caller
var str strings.Builder
str.WriteString(message + "\nTraceback:")
for _, pc := range pcs[:n] {
fn := runtime.FuncForPC(pc)
file, line := fn.FileLine(pc)
str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
}
return str.String()
}
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("%s", err)
log.Printf("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}