七天实现一个go web框架

引流

  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. 欢迎来访!在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Generalzy

文章对您有帮助,倍感荣幸

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

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

打赏作者

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

抵扣说明:

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

余额充值