Gee 项目复现

news2024/11/16 23:56:40

序言

复现:原链接

一个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())

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/676740.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于边界点优化和多步路径规划的机器人自主探索

论文题目&#xff1a;Autonomous Robotic Exploration Based on Frontier Point Optimization and Multistep Path Planning 中文题目&#xff1a;基于边界点优化和多步路径规划的机器人自主探索 作者&#xff1a;Baofu Fang &#xff1b;Jianfeng Ding ; Zaijun Wang 作者机…

5.5.2 IPv6数据报格式

5.5.2 IPv6数据报格式 首先我们来回忆一下IPv4数据报首部格式&#xff08;5.2.3 IP数据报&#xff08;一&#xff09;IP数据报的格式&#xff09;&#xff0c;包括20个字节的固定部分和长度可变的选项部分&#xff0c;如图 红色方框标注的是在IPv6中会消失的字段&#xff0c;椭…

小白也会的------新建Python虚拟环境,查看该虚拟环境的路径,将该虚拟环境的所有库和版本号导出到一个 requirements.txt 文件中

我的目录标题 1、新建Python虚拟环境2、查看该虚拟环境的路径3、将该虚拟环境的所有库和版本号导出到一个 requirements.txt 文件中4、如果你只需要将当前虚拟环境中安装的所有库和版本号导出到一个 requirements.txt 文件中&#xff0c;而不需要包括每个库的来源&#xff0c;可…

KMP算法基础

前言 KMP算法是我们数据结构串中最难也是最重要的算法。难是因为KMP算法的代码很优美简洁干练&#xff0c;但里面包含着非常深的思维。真正理解代码的人可以说对KMP算法的了解已经相当深入了。而且这个算法的不少东西的确不容易讲懂&#xff0c;很多正规的书本把概念一摆出直接…

C++——命名空间(namespace)

目录 1. C语言命名冲突 2. 命名空间定义 3. 命名空间使用 可能大家在看别人写的C代码中&#xff0c;在一开始会包这个头文件&#xff1a;#include<iostream> 这个头文件等价于我们在C语言学习到的#include<stdio.h>&#xff0c;它是用来跟我们的控制台输入和输出…

带你见见红黑树-概念+插入篇

写的不好&#xff0c;见谅~ 目录 概念理解 红黑树规则 AVL树与红黑树的相爱相杀 红黑树的插入时的上色与旋转。 不上色&#xff08;shǎi&#xff09; 情况一&#xff1a;空树 情况二&#xff1a;非空树&#xff0c;父节点为黑 上色&#xff08;shǎi&#xff09; 情况…

【Linux】深入理解文件系统

系列文章 收录于【Linux】文件系统 专栏 关于文件描述符与文件重定向的相关内容可以移步 文件描述符与重定向操作。 可以到 浅谈文件原理与操作 了解文件操作的系统接口。 想深入理解文件缓冲区还可以看看文件缓冲区。 目录 系列文章 磁盘 结构介绍 定位数据 抽象管理…

【Linux】MySQL 高级 SQL 语句 (二)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 MySQL 高级 SQL 语句 连接查询CREATE VIEW 视图UNION 联集交集值无交集值CASE空值(NULL) 和 无值() 的区别正则表达式 连接查询 mysql> select * from xjz; #xjz表格 ---…

Linux5.8 MySQL主从复制与读写分离

文章目录 计算机系统5G云计算第四章 LINUX MySQL主从复制与读写分离一、概述及原理1&#xff09;什么是读写分离2&#xff09;为什么要读写分离呢3&#xff09;什么时候要读写分离4&#xff09;主从复制与读写分离5&#xff09;mysql支持的复制类型6&#xff09;主从复制的工作…

Rust语言从入门到入坑——(5)Rust 所有权

文章目录 0 引入1、所有权2、内存和分配3、移动与克隆3.1、移动3.2、克隆 4、引用与租借4.1、引用4.1、垂悬引用 5、函数中变量5.1 参数变量5.2 、返回值变量 0 引入 主要介绍Rust所有权的知识&#xff0c;涉及到变量的作用域&#xff0c;内存释放机制&#xff0c;移动&#x…

Python|Pyppeteer启动浏览器窗口,右侧出现空白区域怎么解决?(13)

前言 本文是该专栏的第13篇,结合优质项目案例持续分享Pyppeteer的干货知识,记得关注。 有些同学可能在使用pyppeteer的时候,在配置项里面,明明已经设置好了窗口最大化,而启动Chromium窗口,打开的窗口最右侧却是一大片空白区域,具体如下图所示: 那么,出现上述情况,需…

AutoGPT 英文版安装过程

自从2022年11月chatGPT的发布3.0GPT大模型&#xff0c;在中国掀起一股AI学习热潮&#xff0c;国内百度2023年4月份发布文心一言&#xff0c;把AI推上另一个高潮&#xff0c;最直接的是问答&#xff0c;我输入一句话&#xff0c;AI帮生成一段文字或一个视频&#xff0c;但是国内…

畅捷通T+ 反序列化漏洞复现(QVD-2023-13615)

0x01 产品简介 畅捷通 T 是一款基于互联网的新型企业管理软件&#xff0c;功能模块包括&#xff1a;财务管理、采购管理、库存管理等。主要针对中小型工贸和商贸企业的财务业务一体化应用&#xff0c;融入了社交化、移动化、物联网、电子商务、互联网信息订阅等元素。 0x02 漏…

Chat2DB数据AI工具开源!对数据分析师行业影响如何?

大家好&#xff0c;我是千与千寻&#xff0c;千寻目前在互联网公司担任算法工程师&#xff0c;也要经常性的和数据打交道。那么数据都存放在哪里&#xff1f;当然是数据库啦&#xff01; 说到数据库&#xff0c;我们就不得不提到一种编程语言——SQL数据语言&#xff0c;后端程…

2023年最新项目管理工具排名推荐,助你提升项目效率!

在当今快速发展的互联网时代&#xff0c;项目管理工具已经成为了越来越多企业和团队必不可少的工具之一。好的项目管理工具能够帮助团队更加高效地协同工作&#xff0c;提高工作效率&#xff0c;节省时间和成本&#xff0c;从而使得整个项目可以更快地达成预期目标。现在让我们…

微信为什么使用 SQLite 保存聊天记录?

概要 SQLite 是一个被大家低估的数据库&#xff0c;但有些人认为它是一个不适合生产环境使用的玩具数据库。事实上&#xff0c;SQLite 是一个非常可靠的数据库&#xff0c;它可以处理 TB 级的数据&#xff0c;但它没有网络层。接下来&#xff0c;本文将与大家共同探讨 SQLite 在…

【Diffusion模型系列1】DDPM: Denoising Diffusion Probabilistic Models

0. 楔子 Diffusion Models(扩散模型)是在过去几年最受关注的生成模型。2020年后&#xff0c;几篇开创性论文就向世界展示了扩散模型的能力和强大: Diffusion Models Beat GANs on Image Synthesis(NeurIPS 2021 Spotlight, OpenAI团队, 该团队也是DALLE-2的作者)[1] Various…

阿里云国际站代理商:如何优化阿里云服务器的性能和响应速度?有哪些调优策略和建议?

随着互联网的发展&#xff0c;阿里云服务器已经成为很多企业和个人的首选解决方案。然而&#xff0c;面对不断增长的需求和复杂的网络环境&#xff0c;如何优化阿里云服务器的性能和响应速度&#xff0c;提高用户体验&#xff0c;是很多用户关心的问题。本文将从以下几个方面&a…

上海阿里云代理商:如何保护阿里云服务器中的敏感数据?有哪些加密和访问控制措施?

如何保护阿里云服务器中的敏感数据&#xff1f;有哪些加密和访问控制措施&#xff1f;   一、阿里云服务器安全概述   阿里云服务器作为云计算服务的主要产品&#xff0c;其安全性备受用户关注。在实际使用中&#xff0c;保护服务器中的敏感数据是至关重要的&#xff0c;而…

Tkinter之GUI界面布局介绍

Tkinter之GUI界面布局介绍 关于Python 的Tkinter窗口基础可参见https://blog.csdn.net/cnds123/article/details/127227651 Tkinter 本身没有提供拖拽放置控件的方式创建 GUI 界面&#xff0c;而是提供了pack、grid和place三种几何管理器&#xff08;geometry manager&#x…