Gee 项目复现

序言

复现:原链接

一个Web框架需要支持的功能,

  • 路由,请求到响应函数的映射,支持动态路由如hello/:name,hello/*
  • 模板,使用内置模板引擎渲染机制。
  • 鉴权:分组
  • 插件:中间件

第一天 HTTP基础

启动http服务

一个启动http服务的示例,

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {

	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/hello", helloHandler)

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}
// return headers
func helloHandler(writer http.ResponseWriter, request *http.Request) {
	for k, v := range request.Header {
		fmt.Fprintf(writer, "Header[%q] = %q\n", k, v)
	}
}

// return the r.URL.Path
func indexHandler(writer http.ResponseWriter, request *http.Request) {
	fmt.Fprintf(writer, "URL.Path = %q\n", request.URL.Path)

}

可以看到启动服务时使用的是,http.ListenAndServe(":8080", nil),传入了nil,实际上可以传入一个handler,用于处理请求,也就是所有请求的入口。

可以看源码,实际上就是一个接口,

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

于是我们可以实现一个处理器用来处理链接

type Engine struct {
}

func (e *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 := new(Engine)
	if err := http.ListenAndServe(":8080", engine); err != nil {
		log.Fatal(err)
	}
}

构建雏形

组织代码

gee/
  |--gee.go
  |--go.mod
main.go
go.mod

其中go.mod内容为

module exmaple

go 1.20

require (
	gee v0.0.0
)

replace (
	gee => ./gee
)

gee指向./gee,相对路径包的引用方式。

gee/go.mod的内容

module gee

go 1.20

主函数

main.go

package main

import (
	"fmt"
	"gee"
	"net/http"
)

func main() {
	r := gee.New()

	r.GET("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
	})
	r.GET("/hello", func(w http.ResponseWriter, r *http.Request) {
		for k, v := range r.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	})
	r.Run(":8080")
}

这就与gin的使用方法很像了,使用new创建gee实例,如何处理动态路由GET添加了路由,

GET方法和Run方法都在gee中实现。

最后用Run启动服务。

gee.go的实现

package gee

import (
	"fmt"
	"net/http"
)

// 定义HandlerFunc 类型
type HandlerFunc func(w http.ResponseWriter, r *http.Request)

// 定义引擎,其中有路由表
type Engine struct {
	router map[string]HandlerFunc
}

// 创建一个引擎
func New() *Engine {
	return &Engine{router: make(map[string]HandlerFunc)}
}

// 添加路由
func (e *Engine) addRouter(method string, pattern string, handler HandlerFunc) {
	key := method + "-" + pattern
	e.router[key] = handler
}

// 添加 GET请求路由
func (e *Engine) GET(pattern string, handler HandlerFunc) {
	//调用 添加路由方法
	e.addRouter("GET", pattern, handler)
}

// 添加 POST请求路由
func (e *Engine) POST(pattern string, handler HandlerFunc) {
	e.addRouter("POST", pattern, handler)
}

// 开启一个http server
func (e *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, e)
}

// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	key := req.Method + "-" + req.URL.Path
	if handler, ok := e.router[key]; ok {
		handler(w, req)
	} else {
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, "404 NOT FOUND %q\n", req.URL.Path)
	}
}

引擎中有路由表,负责实现请求到回调函数的映射,而添加路由的方法则是通过,GET,POST等方法实现的。

第二天 上下文

主要增加功能

  • router独立出来,方便后续增强
  • 设计上下文context,即封装RequestResponse,提供对JSONHTML的支持

使用效果

package main

import (
	"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) {
		// xx/hello?name = tom
		c.String(http.StatusOK, "hello %s, your location %s ", 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(":8080")
}

设计Context

为什么?

对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是两个部分,我们应该构建一个完整的响应,包括需要的各部分内容。headers,statusCode

封装前

obj = map[string]interface{}{
    "name": "geektutu",
    "password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
    http.Error(w, err.Error(), 500)
}

封装后

c.JSON(http.StatusOK, gee.H{
    "username": c.PostForm("username"),
    "password": c.PostForm("password"),
})

对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载

封装实现

gee/context.go

package gee

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// 定义Json封装类型,更加简洁
type H map[string]interface{}

type Context struct {
	//原始对象
	Writer http.ResponseWriter
	Req    *http.Request
	//请求信息
	Path   string
	Method string
	//响应信息
	StatusCode int
}

func newContext(w http.ResponseWriter, r *http.Request) *Context {
	return &Context{
		Writer: w,
		Req:    r,
		Path:   r.URL.Path,
		Method: r.Method,
	}
}

// 查询post参数的值
func (c *Context) PostForm(key string) string {
	return c.Req.FormValue(key)
}

// 查询get url的参数
func (c *Context) Query(key string) string {
	return c.Req.URL.Query().Get(key)
}

// 填状态码
func (c *Context) Status(code int) {
	c.StatusCode = code
	c.Writer.WriteHeader(code)
}

// 设置响应头
func (c *Context) SetHeader(key string, 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)

	if err := encoder.Encode(obj); err != nil {
		http.Error(c.Writer, err.Error(), 500)
	}
}

// 设置响应数据
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

将路由提取出来,单独实现,gee/router.go,便于功能增强,

微调实现,将handler变为 context,小写,封装在包内使用

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) addRouter(method string, 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"
)

// 定义HandlerFunc 类型
type HandlerFunc func(ctx *Context)

// 定义引擎,其中有路由表
type Engine struct {
	router *router
}

// 创建一个引擎
func New() *Engine {
	return &Engine{router: newRouter()}
}

// 添加 GET请求路由
func (e *Engine) GET(pattern string, handler HandlerFunc) {
	//调用 添加路由方法
	e.router.addRouter("GET", pattern, handler)
}

// 添加 POST请求路由
func (e *Engine) POST(pattern string, handler HandlerFunc) {
	e.router.addRouter("POST", pattern, handler)
}

// 开启一个http server
func (e *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, e)
}

// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c := newContext(w, r)
	e.router.handle(c)
}

将路由独立出来了,单独实现,引擎中只需要一个指针即可。

第三天 前缀树路由

内容:

  • 使用Trie树实现动态路由解析
  • 支持两种模:name和*filepath

现在路由的缺陷,

现在使用map[string]handlerFun确实比较高效一一对应,但是只能支持静态路由。

例如想要实现/hello/:name这样的动态路由则不行,

动态路由指一条路由规则可以匹配某一类型而不是一条固定的路由。

例如: /hello/:name,可以匹配/hello/Jack,/hello/Tom.

动态路由的实现方式有许多方法。

比如:正则gorouter

前缀树是也是一个好的解决方案,以/作为分隔,

前缀树目录管理

HTTP恰好是用/分割路径的,动态路由的两个功能

  • 参数匹配:,例如/p/:lang/doc可以匹配/p/c/doc/p/go/doc
  • 通配符*,例如/static/*filepath,…也能递归匹配子路径。

Trie树的实现

包含有待匹配的路由,路由中的一部分,字节点,是否精准匹配;

type node struct {
	pattern  string  // 待匹配的路由,
	part     string  //路由中的一部分
	children []*node //子节点
	isWild   bool    //是否精准匹配
}

首先实现查找功能,给定一个part,查找所有满足条件的子节点,

// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(part string) *node {
	for _, child := range n.children { //遍历子节点
		if child.part == part || child.isWild { //子节点是否与当前路径部分相等, child.isWild ?
			return child
		}
	}
	return nil
}

// 返回所有匹配成功的节点,用于查找
func (n *node) mathChildren(part string) []*node {
	nodes := make([]*node, 0)

	for _, child := range n.children {
		if child.part == part || child.isWild {
			nodes = append(nodes, child)
		}
	}
	return nil
}

对于路由来说,最重要的当然是注册与匹配。对应Trie树就是插入和查询功能。

插入功能,递归查找每一层的节点,如果没有匹配到当前part,则创建节点。

查询功能,递归查询每层的节点,退出规则为匹配到*,匹配失败,或匹配到第len(parts)层节点。

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
}

Router

引用到Router中去,

roots存储每一种请求方式的Trie树根节点。

暂时不明白

package gee

import (
	"log"
	"net/http"
	"strings"
)

type router struct {
	roots    map[string]*node
	handlers map[string]HandlerFunc
}

func newRouter() *router {
	return &router{
		roots:    make(map[string]*node),
		handlers: make(map[string]HandlerFunc)}
}

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) getRoute(method string, 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
}

func (r *router) addRouter(method string, pattern string, handler HandlerFunc) {
	parts := parsePattern(pattern)

	key := method + "-" + pattern
	_, ok := r.roots[method]
	if !ok {
		r.roots[method] = &node{}
	}
	r.roots[method].insert(pattern, parts, 0)
	r.handlers[key] = handler
}

Context 与 handle的变化

gee/context.go

type Context struct {
	//原始对象
	Writer http.ResponseWriter
	Req    *http.Request
	//请求信息
	Path   string
	Method string
	Params map[string]string
	//响应信息
	StatusCode int
}

func (c *Context) Param(key string) string {
	value, _ := c.Params[key]
	return value
}

gee/router.go

func (r *router) handle(c *Context) {

	n, params := r.getRoute(c.Method, c.Path)

	if n != nil {
		log.Printf("Status %d %s", c.StatusCode, c.Path)
		c.Params = params
		key := c.Method + "-" + n.pattern
		r.handlers[key](c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		log.Printf("Status %d %s", c.StatusCode, c.Path)
	}

}

这一章最难理解,前缀树还不太熟悉,掌握插入、查找方法应该能增强理解。

第四天 分组控制

实现路由的分组控制,

就是加前缀

分组的意义

对于不同的组采取不同的策略

  • /post开头的路由匿名可访问。
  • /admin开头的路由需要鉴权。
  • /api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

分组嵌套

分组需要有前缀,支持嵌套要知道父节点是谁,存储中间件。

gee/gee.go

type RouterGroup struct {
	prefix string

	middlewares []HandlerFunc
	parent      *RouterGroup
	engine      *Engine //共享一个engine
}

同时对Engine作修改:

// 定义引擎,其中有路由表
type Engine struct {
	*RouterGroup
	router *router
	group  []*RouterGroup //存储所有的分组
}

将所有和路由相关的函数,都交给RouterGroup实现

gee.go

package gee

import (
	"log"
	"net/http"
)

type RouterGroup struct {
	prefix string

	middlewares []HandlerFunc
	parent      *RouterGroup
	engine      *Engine //共享一个engine
}

// 定义HandlerFunc 类型
type HandlerFunc func(ctx *Context)

// 定义引擎,其中有路由表
type Engine struct {
	*RouterGroup
	router *router
	groups []*RouterGroup //存储所有的分组
}

// 创建一个引擎
func New() *Engine {
	engine := &Engine{router: newRouter()}
	engine.RouterGroup = &RouterGroup{
		engine: engine,
	}
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}
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
	log.Printf("Route %4s - %s", method, pattern)
	group.engine.router.addRoute(method, pattern, handler)
}

// 添加 GET请求路由
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
	//调用 添加路由方法
	group.addRoute("GET", pattern, handler)
}

// 添加 POST请求路由
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
	//调用 添加路由方法
	group.addRoute("POST", pattern, handler)
}

// 开启一个http server
func (e *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, e)
}

// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c := newContext(w, r)
	e.router.handle(c)
}

main.go

func main() {
	r := gee.New()

	r.GET("/", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
	})

	v1 := r.Group("/v1")
	{
		v1.GET("/", func(c *gee.Context) {
			c.HTML(http.StatusOK, "<h1>Hello Gee Group</h1>")
		})
		v1.GET("/hello", func(c *gee.Context) {
			// expect /hello/tom
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
		})
	}
	v2 := r.Group("/v2")
	{
		v2.POST("/login", func(c *gee.Context) {
			c.JSON(http.StatusOK, gee.H{
				"username": c.PostForm("username"),
				"password": c.PostForm("password"),
			})
		})
	}

	r.Run(":8080")
}

简单来说多了一个前缀,支持嵌套,每个分组里有父指针,有子分组数组。

第五天 中间件

内容:

  • 设计并实现web框架中间件机制
  • 实现通用的Logger中间件,记录请求响应时间。

中间件是什么

非业务的技术组件。

web框架不可能实现所有功能,所以提供一个插口,

可以允许用户自定义功能。

关键点

  • 放在哪?太底层会比较复杂,太靠近用户会与手动调用没区别。
  • 中间件的输入是什么?决定了扩展能力

中间件设计

参考Gin

中间件的定义与Handler一样,处理第输入是Context对象,允许用户做一些自己的操作。

用户通过(*Context).Next()函数,中间件可以等待Handler处理完成后做一些操作。

我们的希望:

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))
	}
}

中间件应该在RouterGroup上,最顶层相当于都使用,

gee/context.go

type Context struct {
	//原始对象
	Writer http.ResponseWriter
	Req    *http.Request
	//请求信息
	Path   string
	Method string
	Params map[string]string
	//响应信息
	StatusCode int

	//中间件
	handlers []HandlerFunc
	index    int
}


func newContext(w http.ResponseWriter, r *http.Request) *Context {
	return &Context{
		Writer: w,
		Req:    r,
		Path:   r.URL.Path,
		Method: r.Method,
		index:  -1, //表示执行到第几个中间件了
	}
}

func (c *Context) Next() {
	c.index++
	s := len(c.handlers)
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

代码实现

定义Use函数,将中间件用到某个组。

还需要改变ServeHTTP的实现,将某组的middlerwares传递给Context

gee/gee.go

// 使用中间件
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
	group.middlewares = append(group.middlewares, middlewares...)
}

// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var middlewares []HandlerFunc
	for _, group := range e.groups {
		if strings.HasPrefix(r.URL.Path, group.prefix) {
			middlewares = append(middlewares, group.middlewares...)
		}
	}

	c := newContext(w, r)
	c.handlers = middlewares
	e.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])
		log.Printf("Status %d %s", c.StatusCode, c.Path)
	} else {
		c.handlers = append(c.handlers, func(ctx *Context) {
			ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		})
		log.Printf("Status %d %s", c.StatusCode, c.Path)
	}
	c.Next()
}

真正执行Handler是在c.Next()中。

使用

package main

import (
	"gee"
	"log"
	"net/http"
	"time"
)

func onlyForV2() gee.HandlerFunc {
	return func(ctx *gee.Context) {
		t := time.Now()
		ctx.Fail(500, "Internal Server Error")

		log.Printf("[%d] %s in %v for Group V2", ctx.StatusCode, ctx.Req.RequestURI, time.Since(t))

	}
}

func main() {
	r := gee.New()
	r.Use(gee.Logger())
	r.GET("/", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
	})

	v1 := r.Group("/v1")
	{
		v1.GET("/", func(c *gee.Context) {
			c.HTML(http.StatusOK, "<h1>Hello Gee Group</h1>")
		})
		v1.GET("/hello", func(c *gee.Context) {
			// expect /hello/tom
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
		})
	}
	v2 := r.Group("/v2")
	v2.Use(onlyForV2())
	{
		v2.GET("/hello/:name", func(c *gee.Context) {
			// expect /hello/tom
			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(":8080")
}

第六天 模板 Template

内容:

  • 实现静态资源服务
  • 支持HTML模板渲染

服务端渲染

现在大多数Web服务都是前后端分离的。

对爬虫不友好。因为访问的是渲染前的页面。

实现服务端渲染。

静态文件

将静态资源返回给客户端。

文件返回net/http库已经实现了。

需要做的是找到服务器上,文件的真实位置。然后交给http.FileServer即可。

gee/gee.go

// 静态文件处理器
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) func(c *Context) {
	absoluePath := path.Join(group.prefix, relativePath)
	fileServer := http.StripPrefix(absoluePath, http.FileServer(fs))

	return func(c *Context) {
		file := c.Param("filepath")
		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")
	group.GET(urlPattern, handler)
}

将文件映射,并添加处理函数。

HTML模板渲染

使用html/template模板,

gee/gee.go

// 定义引擎,其中有路由表
type Engine struct {
	*RouterGroup
	router        *router
	groups        []*RouterGroup //存储所有的分组
	htmlTemplates *template.Template
	funcMap       template.FuncMap
}

func (e *Engine) SetFuncMap(funcMap template.FuncMap) {
	e.funcMap = funcMap
}
func (e *Engine) LoadHTMLGlob(pattern string) {
	e.htmlTemplates = template.Must(template.New("").Funcs(e.funcMap).ParseGlob(pattern))
}

为 Engine 示例添加了 *template.Templatetemplate.FuncMap对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。

给用户分别提供了设置自定义渲染函数funcMap和加载模板的方法。

(*Context).HTML()方法修改。

gee/context.go

type Context struct {
    ...
	engine   *Engine //添加Engine指针
}

// 设置响应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())
	}
}

Context 中添加了成员变量 engine *Engine,这样就能够通过 Context 访问 Engine 中的 HTML 模板,

实例化 Context 时,还需要给 c.engine 赋值。

gee/gee.go

// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	...
	c := newContext(w, r)
	c.handlers = middlewares
	c.engine = e
	e.router.handle(c)
}

使用

package main

import (
	"fmt"
	"gee"
	"html/template"
	"log"
	"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 onlyForV2() gee.HandlerFunc {
	return func(ctx *gee.Context) {
		t := time.Now()
		//ctx.Fail(500, "Internal Server Error")

		log.Printf("[%d] %s in %v for Group V2", ctx.StatusCode, ctx.Req.RequestURI, time.Since(t))

	}
}

func main() {
	r := gee.New()
	r.Use(gee.Logger())

	r.SetFuncMap(template.FuncMap{
		"FormatAsDate": FormatAsDate,
	})

	r.LoadHTMLGlob("templates/*")
	r.Static("/assets", "./static")

	stu1 := &student{Name: "Tom", Age: 22}
	stu2 := &student{Name: "Judy", Age: 23}

	r.GET("/", func(c *gee.Context) {
		c.HTML(http.StatusOK, "css.tmpl", nil)
	})

	v1 := r.Group("/v1")
	{
		v1.GET("/student", func(c *gee.Context) {
			c.HTML(http.StatusOK, "arr.tmpl", gee.H{
				"title":  "gee",
				"stuArr": [2]*student{stu1, stu2},
			})
		})
		v1.GET("/hello", func(c *gee.Context) {
			// expect /hello/tom
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
		})
	}
	v2 := r.Group("/v2")
	v2.Use(onlyForV2())
	{
		v2.GET("/date", func(ctx *gee.Context) {
			ctx.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
				"title": "gee",
				"now":   time.Date(2023, 6, 23, 14, 22, 0, 0, time.UTC),
			})
		})
		v2.GET("/hello/:name", func(c *gee.Context) {
			// expect /hello/tom
			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(":8080")
}

第七天

内容:

  • 实现错误处理机制

Panic

go语言中,常见的错误处理是返回error,由调用者决定后续如何处理。

如果无法恢复,则手动触发panic;数组越界自动触发panic

defer

panic会导致程序被终止,但在退出前,会先处理已经defer的任务,执行完成再退出。

recover

Go提供了recover函数,避免因为panic导致整个程序终止,recover函数只在defer中失效。

// hello.go
func test_recover() {
	defer func() {
		fmt.Println("defer func")
		if err := recover(); err != nil {
			fmt.Println("recover success")
		}
	}()

	arr := []int{1, 2, 3}
	fmt.Println(arr[4])
	fmt.Println("after panic")
}

func main() {
	test_recover()
	fmt.Println("after recover")
}
go run hello.go 
defer func
recover success
after recover

Gee错误处理

使用中间件增强gee框架的能力。

gee/recovery.go

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()
	}
}

Recovery 的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error

使用

r.Use(gee.Recovery())

lo.go
func test_recover() {
defer func() {
fmt.Println(“defer func”)
if err := recover(); err != nil {
fmt.Println(“recover success”)
}
}()

arr := []int{1, 2, 3}
fmt.Println(arr[4])
fmt.Println("after panic")

}

func main() {
test_recover()
fmt.Println(“after recover”)
}


```go
go run hello.go 
defer func
recover success
after recover

Gee错误处理

使用中间件增强gee框架的能力。

gee/recovery.go

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()
	}
}

Recovery 的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error

使用

r.Use(gee.Recovery())
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

都学点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值