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响应。
  1. 对Web服务来说,无非是根据请求 *http.Request,构造响应 http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
  2. 针对使用场景,封装 *http.Requesthttp.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()
	}
}

Go项目-Gee Web框架
https://zhangzhao219.github.io/2022/11/21/Go/Go-Project-Gee/
作者
Zhang Zhao
发布于
2022年11月21日
许可协议