目录
- 引流
- 为什么要用web框架
- 复习下net/http库以及http.Handler接口
- 代码结构
- General.go
- 启动!
- 上下文
- 必要性
- 封装前
- context.go
- 拆分router
- 封装后
- 启动!
- 前缀树路由
- Trie 树
- 目标
- 实现前缀树
- 修改router
- 改变ServeHTTP实现
- 分组控制
- Group对象的属性
- 其余实现
- 中间件
- 实现
- 其他实现
- 问题
- 模板
- 全局异常捕获
- 总结
引流
- 项目实战,引用
极客兔兔
大佬的七天系列开源图书。 - 图书地址:7天用Go从零实现
为什么要用web框架
-
当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。
-
net/http提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。一些Web开发中简单的需求并不支持,需要手工实现。
- 动态路由:例如hello/:name,hello/*这类的规则。
- 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。
- 模板:没有统一简化的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"))
}
上下文
- 注意:在 WriteHeader() 后调用 Header().Set 是不会生效的。
- 正确的调用顺序应该是Header().Set 然后WriteHeader() 最后是Write()
必要性
-
对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。
-
因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。
-
针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
-
针对使用场景,封装*http.Request和http.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。
-
对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。
-
因此,设计 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"))
}
启动!
前缀树路由
- 使用map存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。
- 那如果想支持类似于/hello/:name这样的动态路由怎么办呢?所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。例如/hello/:name,可以匹配/hello/geektutu、hello/jack等。
Trie 树
-
实现动态路由最常用的数据结构,被称为前缀树(Trie树)。
-
每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。
-
如下路由:
/:lang/doc
/:lang/tutorial
/:lang/intro
/about
/p/blog
/p/related
HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束404。
目标
实现的动态路由具备以下两个功能:
- 参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc 和 /p/go/doc。
- 通配*。例如 /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对象的属性
- 前缀(prefix),比如/,或者/api;
- 要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁:指向engine的指针
- 中间件(middlewares)。
- 上一级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)
}
中间件
- 中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。
- 中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。
- 过调用(*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...)
}
- Use方法将定义好的中间件交给Group,也就是交给专门的Group或Engine(引擎可以看作最大的Group),作为应用到该组的中间件。
- ServeHTTP方法根据Group将对应的中间件取出来交给ctx上下文
- handle方法将所有HandlerFunc类型包括view都加入到中间件列表,即把view也当作中间件的一层,这样调用就可以是handler(ctx)。
- 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))
}
}
-
在 trace() 中,调用了 runtime.Callers(3, pcs[:]),Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func。因此,为了日志简洁一点,跳过前 3 个 Caller。
-
接下来,通过 runtime.FuncForPC(pc) 获取对应的函数,在通过 fn.FileLine(pc) 获取到调用该函数的文件名和行号,打印在日志中。
总结
- 做出了一个简单的web框架,实现了动态路由参数匹配,路由分组,中间件等功能。
- 实现了一个简易的前缀树路由,对结构嵌套应用递归等有了更多理解。
- 理解了为什么中间件和视图函数都是HandlerFunc类型,大佬的诸多设计巧夺天工。
- 不足之处是,对runtime库和调用堆栈不甚理解,所以最后一节的异常捕获是将代码copy下来的,接下来需要学习runtime库。
- 至此,
General web framework
耗时两天,完美收官,github地址:https://github.com/Generalzy/General-web-framework - 欢迎来访!