七天实现一个go web框架

news2024/11/25 8:18:22

目录

  • 引流
  • 为什么要用web框架
  • 复习下net/http库以及http.Handler接口
  • 代码结构
    • General.go
    • 启动!
  • 上下文
    • 必要性
    • 封装前
    • context.go
    • 拆分router
    • 封装后
    • 启动!
  • 前缀树路由
    • Trie 树
    • 目标
    • 实现前缀树
    • 修改router
    • 改变ServeHTTP实现
  • 分组控制
    • Group对象的属性
    • 其余实现
  • 中间件
    • 实现
    • 其他实现
    • 问题
  • 模板
  • 全局异常捕获
  • 总结

引流

  1. 项目实战,引用极客兔兔大佬的七天系列开源图书。
  2. 图书地址:7天用Go从零实现

为什么要用web框架

  1. 当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。

  2. net/http提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。一些Web开发中简单的需求并不支持,需要手工实现。

    1. 动态路由:例如hello/:name,hello/*这类的规则。
    2. 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。
    3. 模板:没有统一简化的HTML机制。

本章节将跟随大佬的脚步开发一个轻量级的web框架:General
Github地址:General

复习下net/http库以及http.Handler接口

log.Fatal(http.ListenAndServe(":9999", nil))

第一个参数是地址,:9999表示在 9999 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。

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

func ListenAndServe(address string, h Handler) error

也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。

package main

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

// Engine 引擎实现了Handler接口用于处理HTTP请求
type Engine struct{}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, request *http.Request) {
	switch request.URL.Path {
	case "/":
		fmt.Fprintf(w, "URL.Path = %q\n", request.URL.Path)
	case "/hello":
		for k, v := range request.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	default:
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", request.URL)
	}
}

func main() {
	engine := new(Engine)
	log.Fatal(http.ListenAndServe(":9999", engine))
}

代码结构

在这里插入图片描述

General.go

package General

import (
	"fmt"
	"net/http"
)

const (
	MethodGet     = http.MethodGet
	MethodHead    = http.MethodHead
	MethodPost    = http.MethodPost
	MethodPut     = http.MethodPut
	MethodPatch   = http.MethodPatch
	MethodDelete  = http.MethodDelete
	MethodConnect = http.MethodConnect
	MethodOptions = http.MethodOptions
	MethodTrace   = http.MethodTrace
)

const keyword = "_"

// HandlerFunc 定义视图函数
type HandlerFunc func(w http.ResponseWriter,request *http.Request)

// Engine 定义General引擎
type Engine struct {
	router map[string]HandlerFunc
}

// New 初始化引擎
func New()*Engine{
	return &Engine{router: make(map[string]HandlerFunc)}
}

// Url 用于注册视图函数与url的映射,灵感来自django
func (e *Engine)Url(method string,pattern string,handler HandlerFunc){
	k:=method+keyword+pattern
	e.router[k]=handler
}

// Path 等效于Url
func (e *Engine)Path(method string,pattern string,handler HandlerFunc){
	e.Url(method,pattern,handler)
}

// Get HTTP GET请求
func (e *Engine)Get(pattern string,handler HandlerFunc){
	e.Url(MethodGet,pattern,handler)
}

// Post HTTP POST请求
func (e *Engine)Post(pattern string,handler HandlerFunc){
	e.Url(MethodPost,pattern,handler)
}

// ServeHTTP 实现Handler接口
func (e *Engine)ServeHTTP(w http.ResponseWriter,request *http.Request){
	k:=request.Method+keyword+request.URL.Path
	if handler,ok:=e.router[k];ok{
		handler(w,request)
	}else{
		_,_=fmt.Fprintf(w,"404 not found: %s \n",request.URL)
	}
}

// Run 开启HTTP服务
func (e *Engine)Run(addr string)error{
	return http.ListenAndServe(addr,e)
}

启动!

package main

import (
	"fmt"
	"github.com/Generalzy/General/General"
	"log"
	"net/http"
)

func main() {
	engine:=General.New()
	engine.Get("/", func(w http.ResponseWriter, request *http.Request) {
		fmt.Println(request.URL)
		fmt.Println(request.URL.Path)
		
		for k, v := range request.Header {
			_,_ = fmt.Fprintf(w, "Header[%q] = %q \n", k, v)
		}
	})
	log.Fatalln(engine.Run(":8080"))
}

在这里插入图片描述
在这里插入图片描述

上下文

  1. 注意:在 WriteHeader() 后调用 Header().Set 是不会生效的。
  2. 正确的调用顺序应该是Header().Set 然后WriteHeader() 最后是Write()

必要性

  1. 对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。

  2. 因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。

  3. 针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

  4. 针对使用场景,封装*http.Request和http.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。

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

  6. 因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

封装前

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

context.go

package General

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

type any interface {}

type H map[string]any

type Context struct {
	Request *http.Request
	Writer http.ResponseWriter

	Path string
	Method string
	Status int
}

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

// SetHeader 设置响应头信息
func (c *Context)SetHeader(key string,val string){
	c.Writer.Header().Set(key,val)
}

// SetStatus 设置响应状态码
func (c *Context)SetStatus(code int){
	c.Status=code
	c.Writer.WriteHeader(code)
}

// String 响应字符格式的快捷操作
func (c *Context)String(code int,format string,value...any){
	c.SetHeader("Content-Type",ContentText)
	c.SetStatus(code)
	_,_ = c.Writer.Write([]byte(fmt.Sprintf(format,value)))
}

// Json 响应json格式的快捷操作
func (c *Context)Json(code int,obj H){
	c.SetHeader("Content-Type",ContentJson)
	c.SetStatus(code)
	data,err:=json.Marshal(obj)
	if err!=nil{
		http.Error(c.Writer,err.Error(),http.StatusInternalServerError)
	}else{
		_,_ = c.Writer.Write(data)
	}
}

// HTML 响应html格式的快捷操作
func (c *Context) HTML(code int, html string) {
	c.SetHeader("Content-Type", "text/html")
	c.SetStatus(code)
	_,_ = c.Writer.Write([]byte(html))
}

拆分router

package General

import "fmt"

type router struct {
	handlers map[string]HandlerFunc
}

func NewRouter()*router{
	return &router{handlers: make(map[string]HandlerFunc)}
}

// Url 用于注册视图函数与url的映射,灵感来自django
func (r *router)Url(method string,pattern string,handler HandlerFunc){
	k:=method+keyword+pattern
	r.handlers[k]=handler
}

// Path 等效于Url
func (r *router)Path(method string,pattern string,handler HandlerFunc){
	r.Url(method,pattern,handler)
}

// handle 路由映射
func (r *router)handle(ctx *Context){
	k:=ctx.Method+keyword+ctx.Path
	if handler,ok:=r.handlers[k];ok{
		handler(ctx)
	}else{
		ctx.String(http.StatusBadRequest,"404 not found: %s \n",ctx.Path)
	}
}

封装后

func main() {
	engine:=General.New()
	engine.Get("/", func(ctx *General.Context) {
		data:=General.H{}
		for k, v := range ctx.Request.Header{
			key:=fmt.Sprintf("Header[%q] = ",k)
			val:=fmt.Sprintf("%q \n",v)
			data[key]=val
		}
		ctx.Json(http.StatusOK,General.H{
			"code":0,
			"data":data,
			"err":"",
		})
	})
	log.Fatalln(engine.Run(":8080"))
}

启动!

在这里插入图片描述

前缀树路由

  1. 使用map存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。
  2. 那如果想支持类似于/hello/:name这样的动态路由怎么办呢?所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。例如/hello/:name,可以匹配/hello/geektutu、hello/jack等。

Trie 树

  1. 实现动态路由最常用的数据结构,被称为前缀树(Trie树)。

  2. 每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。

  3. 如下路由:

    /:lang/doc
    /:lang/tutorial
    /:lang/intro
    /about
    /p/blog
    /p/related
    在这里插入图片描述
    HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束404。

目标

实现的动态路由具备以下两个功能:

  1. 参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc 和 /p/go/doc。
  2. 通配*。例如 /static/*filepath,可以匹配/static/fav.ico,也可以匹配/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径。(用nginx静态代理,我不考虑实现)

map存储uri和handler取值O(1)非常高效,从极客兔兔的教程来看,前缀树只是为了支持动态路由,并且key为请求方式。

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

实现前缀树

package General

type node struct {
	// 待匹配路由
	pattern  string
	// 路由中的一部分
	part     string
	// 子节点列表
	children []*node
	// 是否模糊匹配
	// 路由: "/:name/hello
	// url: "/Generalzy/hello
	// 不添加该字段会导致路由与url无法匹配
	isWild bool
}

// matchChild 寻找匹配到的第一个子节点
func (n *node) matchChild(part string) *node {
	for _, child := range n.children {
		// 精准匹配到路径或本次match是模糊查找的情况下返回节点
		if child.part == part|| child.isWild{
			return child
		}
	}
	return nil
}

// insert 插入节点
// height 路由层数
//
// 以 /:name/hello,Get前缀树 为例:
// 1. pattern="/:name/hello" parts=[":name","hello"] height=0 part=":name" node1={"",":name",[]}
// 结果:root{"","",[node1]}->node1{"",":name",[]}
// 2. pattern="/:name/hello" parts=[":name","hello"] height=1 part="hello" node2={"",":hello",[]}
// 结果:root{"","",[node1]}->node1{"",":name",[node2]}->node2{"","hello",[]}
func (n *node)insert(pattern string,parts []string,height int){
	// 1. 初始化根节点 pattern = / height = 0 parts = []
	// 2. 当height==len(parts)即,到了最后一个节点,将路由整体放入
	if len(parts)==height{
		n.pattern=pattern
		return
	}

	// 获取当前层级的部分url
	part:=parts[height]
	// 遍历根节点寻找part节点
	child := n.matchChild(part)
	// 未找到part节点
	if child == nil{
		// 新建node并且赋值给child
		// 并判断是否有一段路由需要模糊匹配
		child = &node{part: part,isWild: part[0] == ':'}
		n.children = append(n.children, child)
	}

	// 递归下一层路由
	child.insert(pattern, parts, height+1)
}

// matchChildren 所有匹配成功的节点,用于查找
func (n *node) matchChildren(part string) []*node {
	nodes := make([]*node, 0,buf)
	for _, child := range n.children {
		// 当本次需要模糊匹配时,直接将node添加到nodes
		if child.part == part || child.isWild{
			nodes = append(nodes, child)
		}
	}
	return nodes
}


// search 寻找节点
//
// 以 /:name/hello,Get前缀树 为例:
// 1. parts=[":name","hello"] height=0 part=":name" n=root children=[node1{"",":name",[node1]}] child=node1
// 2. parts=[":name","hello"] height=1 part="hello" n=node1 children=[node2{"","hello",[]}] child=node2 返回node2
func (n *node)search(parts []string, height int) *node {
	// 当遍历到最后一个节点时,返回node
	if len(parts) == height{
		if n.pattern==""{
			return nil
		}
		// 直接将根节点返回
		return n
	}

	part:=parts[height]
	children := n.matchChildren(part)

	// 递归下一层级
	for _, child := range children {
		// 接受返回的node
		result := child.search(parts, height+1)
		// 返回找到的第一个节点
		if result != nil {
			return result
		}
	}
	return nil
}

修改router

package General

import (
	"net/http"
	"strings"
)

const buf = 1<<2

type HttpMethod = string

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

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

// parsePattern 将Url按 / 切分
func parsePattern(pattern string)[]string{
	parts:=strings.Split(pattern,"/")
	newParts:=make([]string,0,buf)

	for _,part:=range parts{
		if part!=""{
			newParts=append(newParts,part)
		}
	}
	return newParts
}

// Url 用于注册视图函数与url的映射,灵感来自django
func (r *router)Url(method HttpMethod,pattern string,handler HandlerFunc){
	parts:=parsePattern(pattern)

	k:=method+keyword+pattern
	// 一个方法建立一个前缀树
	// 目前只有get 和 post 两个前缀树
	_,ok:=r.roots[method]
	if !ok{
		r.roots[method]= &node{}
	}
	// 向root插入路由
	r.roots[method].insert(pattern,parts,0)
	r.handlers[k]=handler
}

// getUrlParams 将路由中的动态参数导出
//
func (r *router)getUrlParams(method HttpMethod,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]
			}
		}
		return n, params
	}
	return nil, nil
}

// handle 路由映射
func (r *router)handle(ctx *Context){
	n,params:=r.getUrlParams(ctx.Method,ctx.Path)
	if n!=nil{
		ctx.Params=params
		k:=ctx.Method+keyword+n.pattern
		// node存在说明一定有该路由
		r.handlers[k](ctx)
	} else{
		ctx.String(http.StatusBadRequest,"404 not found: %v \n",ctx.Path)
	}
}

改变ServeHTTP实现

// ServeHTTP 实现Handler接口
func (e *Engine)ServeHTTP(w http.ResponseWriter,request *http.Request){
	// handle 路由映射
	e.router.handle(newContext(w,request))
}

分组控制

Group对象的属性

  1. 前缀(prefix),比如/,或者/api;
  2. 要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁:指向engine的指针
  3. 中间件(middlewares)。
  4. 上一级Group
RouterGroup struct {
	prefix      string
	middlewares []HandlerFunc // support middleware
	parent      *RouterGroup  // support nesting
	engine      *Engine       // all groups share a Engine instance
}

将引擎看作最大的group:

// Engine 定义General引擎
// 将引擎看作最大的Group
type Engine struct {
	*RouterGroup
	router *router
	groups []*RouterGroup
}

其余实现

package General

import (
	"net/http"
)

const keyword = "_"

// HandlerFunc 定义视图函数
type HandlerFunc func(*Context)

// RouterGroup 路由分组
type RouterGroup struct {
	prefix      string
	middlewares []HandlerFunc
	parent      *RouterGroup
	engine      *Engine
}

func (g *RouterGroup)Group(prefix string)*RouterGroup{
	engine:=g.engine

	rg:= &RouterGroup{
		engine: engine,
		// 支持分组的分组...
		prefix: g.prefix+prefix,
		// 谁调用Group,就将谁设置为parent
		parent: g,
	}
	// 把新的group也加进去
	engine.groups = append(engine.groups,rg)
	return rg
}


// Engine 定义General引擎
// 将引擎看作最大的Group
type Engine struct {
	*RouterGroup
	router *router
	groups []*RouterGroup
}

// New 初始化引擎
func New()*Engine{
	engine:=&Engine{router: NewRouter()}
	// 引擎的父节点,代表引擎是root
	// 引擎的engine自然为自己
	// 引擎的前缀自然为""
	engine.RouterGroup = &RouterGroup{engine: engine}
	// 将引擎加入groups
	engine.groups=[]*RouterGroup{engine.RouterGroup}
	return engine
}

// Url 用于注册视图函数与url的映射,灵感来自django
func (g *RouterGroup)Url(method string,pattern string,handler HandlerFunc){
	pattern = g.prefix+pattern
	g.engine.router.Url(method,pattern,handler)
}

// Path 等效于Url
func (g *RouterGroup)Path(method string,pattern string,handler HandlerFunc){
	g.Url(method,pattern,handler)
}

// Get HTTP GET请求
func (g *RouterGroup)Get(pattern string,handler HandlerFunc){
	g.Url(MethodGet,pattern,handler)
}

// Post HTTP POST请求
func (g *RouterGroup)Post(pattern string,handler HandlerFunc){
	g.Url(MethodPost,pattern,handler)
}

// ServeHTTP 实现Handler接口
func (e *Engine)ServeHTTP(w http.ResponseWriter,request *http.Request){
	// handle 路由映射
	e.router.handle(newContext(w,request))
}

// Run 开启HTTP服务
func (e *Engine)Run(addr string)error{
	return http.ListenAndServe(addr,e)
}

中间件

  1. 中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。
  2. 中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。
  3. 过调用(*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。

实现

type Context struct {
	Request *http.Request
	Writer http.ResponseWriter

	// 请求URL
	Path string
	// 请求方式
	Method string
	// 状态码
	Status int
	// URL动态参数
	Params map[string]string

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

func newContext(w http.ResponseWriter, request *http.Request)*Context{
	return &Context{
		Path: request.URL.Path,
		Method: request.Method,
		Request: request,
		Writer: w,
		// 目前没有执行中间件,所以index为-1
		index: -1,
	}
}

// Next 将中间件的控制权交给下一个中间件
func (c *Context)Next(){
	c.index++
	for ;c.index<len(c.middleware);c.index++{
		// 执行下一个中间件
		c.middleware[c.index](c)
	}
}

func (g *RouterGroup)Group(prefix string,middleware...HandlerFunc)*RouterGroup{
	engine:=g.engine
	engine.groups = append(engine.groups,g)

	rg:= &RouterGroup{
		engine: engine,
		// 支持分组的分组...
		prefix: g.prefix+prefix,
		// 谁调用Group,就将谁设置为parent
		parent: g,
		// 将middleware加入
		middlewares: middleware,
	}
	// 把新的group也加进去
	engine.groups = append(engine.groups,rg)
	return rg
}

其他实现

// handle 路由映射
func (r *router)handle(ctx *Context){
	n,params:=r.getUrlParams(ctx.Method,ctx.Path)
	if n!=nil{
		ctx.Params=params
		k:=ctx.Method+keyword+n.pattern
		// node存在说明一定有该路由
		// 将视图函数也作为一个中间件(实际上二者都是Handler)
		ctx.middlewares = append(ctx.middlewares,r.handlers[k])
		// r.handlers[k](ctx)
	} else{
		// 同上,将视图函数也作为一个中间件加入
		ctx.middlewares = append(ctx.middlewares, func(context *Context) {
			ctx.String(http.StatusBadRequest,"404 not found: %v \n",ctx.Path)
		})
	}
	// 调用Next去执行Handler
	// 假设:middlewares = [ m1,m2,view]
	// index = 0,1,2
	// 执行顺序就是: m1(ctx),m2(ctx),view(ctx),m2(ctx),m1(ctx)
	ctx.Next()
}


// ServeHTTP 实现Handler接口
func (e *Engine)ServeHTTP(w http.ResponseWriter,request *http.Request){
	middlewares:=make([]HandlerFunc,0,buf)
	// 遍历路由组
	for _,group:=range e.groups{
		// 如果当前路由是group定义的前缀
		if strings.HasPrefix(request.URL.Path,group.prefix){
			// 将应用到group的middlewares取出来
			middlewares=append(middlewares,group.middlewares...)
		}
	}
	ctx:=newContext(w,request)
	// 将Group的middleware交给ctx
	ctx.middlewares=middlewares
	// handle 路由映射
	e.router.handle(ctx)
}


// Use 将中间件添加入路由
func (g *RouterGroup)Use(middlewares... HandlerFunc){
	g.middlewares=append(g.middlewares,middlewares...)
}
  1. Use方法将定义好的中间件交给Group,也就是交给专门的Group或Engine(引擎可以看作最大的Group),作为应用到该组的中间件。
  2. ServeHTTP方法根据Group将对应的中间件取出来交给ctx上下文
  3. handle方法将所有HandlerFunc类型包括view都加入到中间件列表,即把view也当作中间件的一层,这样调用就可以是handler(ctx)。
  4. Next方法循环中间件列表,依次去执行handler(ctx)。

问题

为什么不将next写为这样:

func (c *Context) Next() {
	c.index++
	c.handlers[c.index](c)
}

大佬解答:
不是所有的handler都会调用 Next()。
手工调用 Next(),一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next(),算是一种兼容性比较好的写法吧。

我的理解:
不是所有的handler里面都会调用Next()去执行定义的其他中间件,如果改成这样,所有的中间件内必须写一遍Next,否则不会继续走下去。比如某个中间件没写Next就会在执行完中间件时候返回,不会执行视图函数。

模板

不考虑实现,略

全局异常捕获

可以借助中间件实现

package General

import (
	"fmt"
	"log"
	"net/http"
	"runtime"
	"strings"
	"time"
)

// trace 获取触发 panic 的堆栈信息
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.String(http.StatusInternalServerError, "Internal Server Error")
			}
		}()
		c.Next()
	}
}

func Logger()HandlerFunc{
	// 2023/01/31 15:20:23  [GET] [500] /panic in 340.1µs
	return func(ctx *Context) {
		start:=time.Now()
		ctx.Next()
		log.Printf(" [%s] [%d] %s in %v \n",ctx.Method,ctx.Status,ctx.Path,time.Since(start))
	}
}
  1. 在 trace() 中,调用了 runtime.Callers(3, pcs[:]),Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func。因此,为了日志简洁一点,跳过前 3 个 Caller。

  2. 接下来,通过 runtime.FuncForPC(pc) 获取对应的函数,在通过 fn.FileLine(pc) 获取到调用该函数的文件名和行号,打印在日志中。

在这里插入图片描述

总结

  1. 做出了一个简单的web框架,实现了动态路由参数匹配,路由分组,中间件等功能。
  2. 实现了一个简易的前缀树路由,对结构嵌套应用递归等有了更多理解。
  3. 理解了为什么中间件和视图函数都是HandlerFunc类型,大佬的诸多设计巧夺天工。
  4. 不足之处是,对runtime库和调用堆栈不甚理解,所以最后一节的异常捕获是将代码copy下来的,接下来需要学习runtime库。
  5. 至此,General web framework耗时两天,完美收官,github地址:https://github.com/Generalzy/General-web-framework
  6. 欢迎来访!在这里插入图片描述

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

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

相关文章

云计算|OpenStack|社区版OpenStack安装部署文档(三 --- 身份认证服务keystone安装部署---Rocky版)

一&#xff0c; 什么是keystone keystone是openstack的关键必选组件之一&#xff0c;其作用是提供身份认证服务&#xff0c;所有的身份认证信息都是存储在controller节点的数据库内。 具体的关于keystone的介绍可以参见官方文档&#xff1a;OpenStack Docs: Identity servic…

设备树中的pin 信息,是在什么时候被初始化的?

一、开发环境 SOC : IMX6ULL系统内核&#xff1a;4.1.15 二、问题描述 Linux 内核提供了pinctrl 和gpio 子系统用于GPIO 驱动。pinctrl_ctrl 子系统从设备树中获取pin 信息&#xff0c;然后配置pin复用 和pin电气特征&#xff08;上/下拉&#xff0c;速度&#xff0c;驱动能…

python小游戏——像素鸟代码开源

♥️作者&#xff1a;小刘在这里 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放&#xff0c;愿所有的美好&#…

特斯拉Q4财报:底部反弹70%,为信仰打call

北京时间2023年1月26日美股盘后&#xff0c;探案君的信仰之股——特斯拉公布了2022年第四季度财报&#xff0c;无论营收还是利润依然吊打华尔街预期&#xff0c;这就是特斯拉&#xff0c;当然这也很特斯拉&#xff01; 一、整体业绩&#xff0c;很特斯拉 营收方面&#xff1a…

JavaScript基础复盘4

JavaScript作用域 JavaScript作用域就是代码名字&#xff08;变量&#xff09;在某个范围内起作用和效果 目的是为了提高程序的可靠性&#xff0c;减少命名冲突。 JS没有块级作用域&#xff0c;{}内写的变量外部也可以使用。 作用域链 作用域链&#xff1a;内部函数访问外部函数…

拯救OOM~ 字节自研 Android 虚拟机内存管理优化黑科技 mSponge

本文描述的虚拟机内存管理优化方案&#xff0c;是从应用侧视角对 Android 虚拟机内存管理进行改造&#xff0c;优化了虚拟机对 LargeObjectSpace 的内存管理策略&#xff0c;间接增加其它内存空间使用上限。改造后的方案&#xff0c;32 位运行环境 LargeObjectSpace 的内存使用…

Aspose.CAD for .NET 23.1.0 Crack

Aspose.CAD for .NET 是一个独立的类库&#xff0c;它增强了 Windows 和 ASP.NET 的应用程序&#xff0c;以便在不需要 AutoCAD 甚至任何其他工作流渲染过程的情况下处理和渲染 CAD 绘图。CAD 类库允许将 DWG、DWT、DWXF、IFC、PLT、DGN、OBJ、STL、IGES、CFF2 文件以及图层和布…

封装一个顺序栈,并封装其相应的操作:判空、入栈、出栈、遍历栈、销毁

main.cpp#include <iostream> #include<fei1.h>using namespace std;int main() {der L;//创建L.date(215);//入栈L.ent(45);L.ent(23);L.ent(98);L.ent(12);//遍历L.trav();// 出栈L.come();L.come();//遍历L.trav();//销毁L.dest();return 0; }fei.cpp#include&l…

docker学习(三):docker的常用命令问问

文章目录前言docker镜像分层加载原理docker镜像commit操作产生新镜像本地镜像发布到阿里云将本地镜像推送到私有库前言 大家好&#xff0c;这是我学习docker系列的笔记文章&#xff0c;目标是掌握docker,为后续学习K8s做准备。本文记录了docker镜像分层加载的原理&#xff0c;…

IDEA SpringBoot热部署

IDEA SpringBoot热部署【自动帮开发者重启 SpringBoot项目&#xff0c;以达到】 1.添加SpringBoot热部署框架支持 在pom.xml中添加如下框架引用&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devt…

Smart HTML Elements R1 2023

Smart HTML Elements R1 2023 增加了Angular 15支持。 添加了对Blazor的Microsoft.NET 7支持。 添加了三维图表组件。 添加了对网格保存/加载状态&#xff08;持久状态&#xff09;的支持。 调度程序 定义特定时段的可用性。 现在可以限制用户输入。 当有多个图例时&a…

长沙23岁应届生做软件测试1年,月入10k!室友悔不当初!

先简单的介绍一下自己&#xff0c;大家好&#xff01;我叫做程双喜&#xff0c;今天23&#xff0c;2021年大学毕业于长沙的一所大专学校&#xff0c;专业是软件测试java专业&#xff0c;三年时间过得很快&#xff0c;转眼间来到了毕业季&#xff0c;自己还是一无所成&#xff0…

Hadoop

1 Hadoop常用端口号 hadoop2.xHadoop3.x访问HDFS端口500709870访问MR执行情况端口80888088历史服务器1988819888客户端访问集群端口90008020 2 Hadoop配置文件 hadoop2.x core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml slaves hadoop3.x core-site.xml、hdfs…

ESP-IDF在VSCode中创建工程文件

1.新建工程项目&#xff0c;打开VSCode&#xff0c;打开命令面板&#xff0c;并输入esp-idf new&#xff0c;执行新建esp-idf项目命令&#xff1a; 2.设置项目名&#xff0c;项目工程目录位置&#xff0c;芯片类型&#xff0c;端口号 3.点击choose Template&#xff0c;从一个…

第八届蓝桥杯省赛 C++ A/B组 - 分巧克力

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;蓝桥杯题解集合 &#x1f4dd;原题地址&#xff1a;后缀表达式 &#x1f4e3;专栏定位&#xff1a;为想参加蓝桥杯的小伙伴整理常考算法题解&#xff0c;祝大…

全志A40i+Logos FPGA开发板(4核ARM Cortex-A7)硬件说明书(下)

前 言 本文档主要介绍板卡硬件接口资源以及设计注意事项等内容,测试板卡为创龙科技旗下的全志A40i+Logos FPGA开发板。 核心板的ARM端和FPGA端的IO电平标准一般为3.3V,上拉电源一般不超过3.3V,当外接信号电平与IO电平不匹配时,中间需增加电平转换芯片或信号隔离芯片。按键…

1、语义软分割算法(Semantatic Human Matting)原理及pytorcch代码详述

一、Semantatic Human Matting原理 Semantatic Human Matting 文章链接请点击这里 第一步:将输入图像送入到T-Net中进行计算,得到三张图:Bs、Fs和Us. T-Net:就是一个编码和解码的过程,和常见的语义分割网络类似 Bs:确定的背景区 Fs:确定的前景区 Us: 不确定的区域 第二…

计算机网络详解--套接字编程

目录 1.什么是网络编程 2.TCP/IP协议 3.Socket套接字 流套接字:使用传输层TCP&#xff08;传输控制协议&#xff09; 数据报套接字:使用传输层UDP&#xff08;用户数据报协议&#xff09; 原始套接字 4.Java数据报套接字通信模型 UDP数据报套接字编程 DatagramSocket A…

IDEA集成Docker配置

首先开启Docker的SSH连接&#xff0c;设置Docker允许远程连接sudo vim /lib/systemd/system/docker.service将ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock注释替换为ExecStart/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run…

GP232RL替代FT232RL国产USB转串口/UART芯片

GP232RL为接口转换芯片&#xff0c;可以实现USB到串行UART接口的转换&#xff0c;也可转换到同步、异步Bit-Bang接口模式。具备可选择的时钟产生输出&#xff0c;以及新的FTDIChip-ID安全加密狗功能。 此外&#xff0c;还提供异步和同步bit bang接口模式。使用GP232RL的USB到串…