7天用Go从零实现Web框架Gee:从net/http到完整框架的设计之路

发布时间:2026/9/21 2:41:21
7天用Go从零实现Web框架Gee:从net/http到完整框架的设计之路 示例工程【免费下载链接】7days-golang7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列项目地址https://gitcode.com/gh_mirrors/7d/7days-golang点击查看免费下载导读本文是 Gee 框架教程系列的序言篇聚焦一个根本问题我们为什么需要 Web 框架框架的核心价值到底是什么文章从 Go 标准库net/http的能力边界出发分析框架需要解决的痛点进而给出 Gee 框架的整体设计理念与七天实现路线图。读完本文你将理解动态路由、分组、中间件、模板渲染等能力为何是框架的必选项并掌握 gee-web 目录下从 day1-http-base 到 day7-panic-recover 各阶段代码的实现脉络与验证方式。设计一个框架先回答框架解决什么问题大部分时候我们需要实现一个 Web 应用第一反应是应该使用哪个框架。不同框架的设计理念和提供的功能差别很大比如 Python 语言的django和flask前者大而全后者小而美。Go 语言也是如此新框架层出不穷例如Beego、Gin、Iris等。那为什么不直接使用标准库而必须使用框架呢在设计一个框架之前我们需要回答框架核心为我们解决了什么问题。只有理解了这一点才能想明白我们需要在框架中实现什么功能。net/http 标准库的能力边界先看看标准库net/http如何处理一个请求func main() { http.HandleFunc(/, handler) http.HandleFunc(/count, counter) log.Fatal(http.ListenAndServe(localhost:8000, nil)) } func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, URL.Path %q\n, r.URL.Path) }net/http提供了基础的 Web 功能即监听端口、映射静态路由、解析 HTTP 报文。但一些 Web 开发中的常见需求并不支持需要手工实现动态路由例如hello/:name、hello/*这类的规则。鉴权没有分组/统一鉴权的能力需要在每个路由映射的 handler 中实现。模板没有统一简化的 HTML 机制。......当我们离开框架、使用基础库时需要频繁手工处理的地方就是框架的价值所在。但并不是每一个频繁处理的地方都适合在框架中完成。从微框架看框架的核心能力Python 有一个很著名的 Web 框架名叫bottle整个框架由bottle.py一个文件构成共 4400 行可以说是一个微框架。理解这个微框架提供的特性可以帮助我们理解框架的核心能力路由(Routing)将请求映射到函数支持动态路由。例如/hello/:name。模板(Templates)使用内置模板引擎提供模板渲染机制。工具集(Utilities)提供对 cookies、headers 等处理机制。插件(Plugin)Bottle 本身功能有限但提供了插件机制可以选择安装到全局也可以只针对某几个路由生效。......这份能力清单基本勾勒出一个 Web 框架的必备轮廓路由、上下文与响应构造、模板渲染、可扩展机制。Gee 的七天实现路线正是沿着这条主线逐步补齐这些能力。Gee 框架小而美向 Gin 致敬这个教程将使用 Go 语言实现一个简单的 Web 框架起名叫做Gee取自geektutu.com的前三个字母。作者第一次接触的 Go Web 框架是GinGin的代码总共是 14K其中测试代码 9K也就是说实际代码量只有5K。Gin与 Python 中的Flask很像小而美。7天实现Gee框架这个教程的很多设计包括源码都参考了Gin因此可以看到很多Gin的影子。时间关系同时为了尽可能地简洁明了框架中的很多部分实现的功能都很简单但是尽可能体现一个框架核心的设计原则。例如Router的设计虽然支持的动态路由规则有限但为了性能考虑匹配算法是用Trie 树前缀树实现的——Router最重要的指标之一便是性能。框架的整体结构演变可以从 gee-web/day4-group/gee/gee.go 中看到最终形态Engine内嵌RouterGroup统一持有router、groups等核心资源所有请求由Engine.ServeHTTP统一接管type ( RouterGroup struct { prefix string middlewares []HandlerFunc // support middleware parent *RouterGroup // support nesting engine *Engine // all groups share a Engine instance } Engine struct { *RouterGroup router *router groups []*RouterGroup // store all groups } )七天路线图从 50 行雏形到完整框架整个教程按七天拆解每一天在上一篇的基础上增加一个核心能力最终形成一个具备静态/动态路由、分组、中间件、模板渲染与错误恢复能力的完整框架。下面结合各阶段文档与源码逐一展开。Day 1前置知识 http.Handler 与框架雏形约 50 行第一天解决框架的入口在哪里的问题。net/http的ListenAndServe第二个参数是一个http.Handler接口package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) } func ListenAndServe(address string, h Handler) error也就是说只要传入任何实现了ServeHTTP的实例所有的 HTTP 请求就都交给了该实例处理。框架要做的事就是实现这个接口把请求全部拦截到自己手里// Engine is the uni handler for all requests type Engine struct{} func (engine *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) log.Fatal(http.ListenAndServe(:9999, engine)) }这一步的意义在于在实现Engine之前只能针对具体的路由写处理逻辑实现Engine之后拥有了统一的控制入口可以自由定义路由映射规则也可以统一添加日志、异常处理等逻辑。随后将代码重新组织成框架雏形目录结构为gee/gee.gomain.go并通过go.mod的replace指令引用本地包从 go 1.11 版本开始引用相对路径的 package 需要使用这种方式module example go 1.13 require gee v0.0.0 replace gee ./gee框架侧的核心实现位于 gee-web/day1-http-base/base3/gee/gee.go用map[string]HandlerFunc作为路由映射表key 由请求方法和静态路由地址构成如GET-/、POST-/helloGET()/POST()注册路由Run()包装ListenAndServeServeHTTP解析请求路径查表执行。此时仅支持静态路由不支持/hello/:name这样的动态路由。完整示例见 gee-web/day1-http-base/base3/main.go。Day 2上下文设计 Context框架代码约 140 行对 Web 服务来说无非是根据请求*http.Request构造响应http.ResponseWriter但这两个对象提供的接口粒度太细。构造一个完整的响应需要同时考虑消息头Header含状态码、Content-Type 等几乎每次都要设置的信息和消息体Body如果不做有效封装框架用户将被迫写大量重复、繁杂且容易出错的代码。用返回 JSON 数据对比封装前后的差距封装前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), })封装接口只是设计 Context 的原因之一。对于框架来说还需要支撑额外的功能将来解析动态路由/hello/:name参数:name的值放在哪框架支持中间件后中间件产生的信息放在哪Context 随着每一个请求的出现而产生、请求的结束而销毁与当前请求强相关的信息都应由 Context 承载。因此设计 Context 结构扩展性和复杂性留在内部对外简化接口。实现见 gee-web/day2-context/gee/context.go要点包括给map[string]interface{}起别名gee.H简化 JSON 构建Context持有Writer、Req并提供Path、Method常用属性提供Query、PostForm参数访问方法提供String/Data/JSON/HTML快速响应方法。同时把路由相关代码独立到 gee-web/day2-context/gee/router.gohandler的参数统一变为*Context方便后续增强。Day 3Trie 树动态路由 Router约 150 行之前用map存储路由表索引高效但只能索引静态路由。要实现/hello/:name这类一条路由规则匹配某一类型路径的动态路由最常用的数据结构是Trie 树前缀树每一个节点的所有子节点都拥有相同的前缀而 HTTP 请求路径恰好是由/分隔的多段构成的每一段可以作为一个节点。例如定义如下路由规则/:lang/doc/:lang/tutorial/:lang/intro/about/p/blog/p/related用前缀树表示后通过树结构查询如果中间某一层的节点都不满足条件就说明没有匹配到的路由。Gee 实现的动态路由支持两种模式参数匹配:例如/p/:lang/doc可以匹配/p/c/doc和/p/go/doc。通配*例如/static/*filepath可以匹配/static/fav.ico也可以匹配/static/js/jQuery.js常用于静态服务器能递归地匹配子路径。节点定义见 gee-web/day3-router/gee/trie.go关键点是isWild字段——part含有:或*时为 true实现模糊匹配type node struct { pattern string // 待匹配路由例如 /p/:lang part string // 路由中的一部分例如 :lang children []*node // 子节点例如 [doc, tutorial, intro] isWild bool // 是否精确匹配part 含有 : 或 * 时为true }配套两个匹配辅助函数matchChild返回第一个匹配成功的节点用于插入和matchChildren返回所有匹配成功的节点用于查找。插入与查询均为递归实现/p/:lang/doc只有在第三层节点doc上才会设置pattern因此可以用n.pattern 判断路由规则是否真正匹配成功例如/p/python能匹配到:lang但:lang的pattern为空匹配失败。路由层实现见 gee-web/day3-router/gee/router.go用roots存储每种请求方式的 Trie 树根节点handlers存储每种请求方式的HandlerFuncparsePattern解析路由规则仅允许一个*遇到*后停止解析getRoute匹配成功后解析:和*的参数并返回 map例如/p/go/doc匹配/p/:lang/doc得到{lang: go}/static/css/geektutu.css匹配/static/*filepath得到{filepath: css/geektutu.css}。Context 相应增加Params字段和Param(key)方法见 gee-web/day3-router/gee/context.go。该阶段的正确性由单元测试保障见 gee-web/day3-router/gee/router_test.goTestParsePattern验证/p/*name/*只解析出p和*nameTestGetRoute验证/hello/geektutu匹配/hello/:name且params[name] geektutuTestGetRoute2验证通配路由的递归匹配。Day 4分组控制 Group约 50 行分组控制是 Web 框架应提供的基础功能之一。真实业务场景中往往某一组路由需要相似的处理例如以/post开头的路由匿名可访问。以/admin开头的路由需要鉴权。以/api开头的路由是 RESTful 接口可对接第三方平台需要三方平台鉴权。大部分情况下的路由分组以相同前缀区分因此 Gee 的分组控制也以前缀来区分并且支持分组的嵌套——作用在/post分组上的中间件也会作用到子分组子分组还可以应用自己特有的中间件。Group 对象需要具备的属性前缀prefix、父分组parent支持嵌套、应用在分组上的中间件middlewares以及指向Engine的指针整个框架的所有资源都由 Engine 统一协调通过它间接访问各种接口。进一步抽象后让Engine 作为最顶层的分组即Engine拥有RouterGroup所有的能力Engine struct { *RouterGroup router *router groups []*RouterGroup // store all groups }New()构造时把engine.RouterGroup初始化为最顶层分组并登记到groupsGroup(prefix)创建新分组时prefix拼接父前缀、记录parent并追加到engine.groupsaddRoute通过group.engine.router.addRoute完成路由注册因此既可以用原来的方式添加路由也可以通过分组添加路由。完整实现见 gee-web/day4-group/gee/gee.go。使用示例详见 gee-web/day4-group/main.gor : gee.New() v1 : r.Group(/v1) { v1.GET(/, func(c *gee.Context) { c.HTML(http.StatusOK, h1Hello Gee/h1) }) v1.GET(/hello, func(c *gee.Context) { c.String(http.StatusOK, hello %s, youre at %s\n, c.Query(name), c.Path) }) }Day 5中间件 Middleware约 50 行中间件简单说就是非业务的技术类组件。框架不可能理解所有业务因而需要提供一个插口允许用户自定义功能嵌入框架仿佛这个功能是框架原生支持的。设计中间件需要思考两个关键点插入点在哪插入点太底层中间件逻辑会非常复杂插入点离用户太近则与用户直接在 Handler 中手工调用一组函数没有多大优势。中间件的输入是什么输入决定了扩展能力暴露的参数太少用户发挥空间有限。Gee 的中间件定义与路由 Handler 一致输入是Context对象插入点是框架收到请求、初始化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)) } }上面的Logger就是框架默认提供的中间件实现见 gee-web/day5-middleware/gee/logger.go。为支持中间件Context 增加了handlers []HandlerFunc与index int两个字段并定义了Next方法index记录当前执行到第几个中间件调用Next时控制权交给下一个中间件直到最后一个即用户注册的 Handler然后再从后往前执行每个中间件在Next之后定义的部分。执行流程可以这样理解假设应用中间件 A、B 和路由 Handlerc.handlers为[A, B, Handler]c.index初始化为 -1最终的调用顺序是part1 - part3 - Handler - part4 - part2恰好满足请求处理前后都能介入的要求。Use方法将中间件应用到某个 Group见 gee-web/day5-middleware/gee/gee.goServeHTTP收到请求后遍历所有 group通过 URL 前缀判断请求适用的中间件并赋值给c.handlersrouter.handle再把路由匹配到的 Handler 追加到c.handlers末尾并调用c.Next()见 gee-web/day5-middleware/gee/router.go。Day 6HTML 模板与静态资源服务端渲染现在流行前后端分离后端提供 RESTful 接口返回结构化数据前端用 AJAX 获取数据并通过 Vue/React 渲染。这种模式前后端解耦、一套后端可同时支撑小程序、移动 App、PC 网页。但页面在客户端渲染对爬虫并不友好短期内爬取服务端直接渲染的 HTML 页面仍是主流。第六天就是介绍框架如何支持服务端渲染。静态文件服务先回顾动态路由里的通配符*路由规则/assets/*filepath可匹配/assets/开头的所有地址参数filepath即文件相对路径。将静态文件放在某个目录如/usr/web下映射到真实文件后交给http.FileServer处理即可。Gee 提供的Static(relativePath, root)方法见 gee-web/day6-template/gee/gee.go把磁盘上的文件夹root映射到路由relativePath例如r : gee.New() r.Static(/assets, /usr/geektutu/blog/static) // 或相对路径 r.Static(/assets, ./static) r.Run(:9999)用户访问localhost:9999/assets/js/geektutu.js最终返回/usr/geektutu/blog/static/js/geektutu.js。底层createStaticHandler先通过fs.Open(file)检查文件存在性与访问权限再交给http.StripPrefix(absolutePath, http.FileServer(fs))处理。HTML 模板渲染Go 内置了text/template和html/template两个模板标准库其中html/template对 HTML 提供了较为完整的支持普通变量渲染、列表渲染、对象渲染等Gee 直接复用了它的能力。Engine增加htmlTemplates *template.Template与funcMap template.FuncMap字段并对外提供SetFuncMap注册自定义模板函数和LoadHTMLGlob加载模板进内存两个方法func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { engine.funcMap funcMap } func (engine *Engine) LoadHTMLGlob(pattern string) { engine.htmlTemplates template.Must(template.New().Funcs(engine.funcMap).ParseGlob(pattern)) }同时 Context 增加engine *Engine成员HTML方法改为根据模板文件名执行渲染见 gee-web/day6-template/gee/context.go。完整示例见 gee-web/day6-template/main.go配合 gee-web/day6-template/templates 目录下的arr.tmpl、css.tmpl、custom_func.tmpl三个模板以及 gee-web/day6-template/static 静态目录使用。Day 7错误恢复 Panic RecoverGo 语言中比较常见的错误处理方法是返回 error由调用者决定后续如何处理。但如果是无法恢复的错误可以手动触发 panic程序运行中出现数组越界等错误panic 也会被触发中止当前程序。defer会在 panic 导致程序中止前先处理完当前协程上已 defer 的任务同一函数中 defer 多个任务会逆序执行recover函数则能避免因 panic 导致整个程序终止且只在 defer 中生效。对一个 Web 框架而言错误处理机制非常必要——框架自身的缺陷或用户传入错误参数引发的空指针、数组越界等异常如果导致系统宕机是不可接受的。第六天实现的框架还没有异常处理机制例如下面的路由处理函数存在数组越界names[100]访问/panic会让整个服务宕掉func main() { r : gee.New() r.GET(/panic, func(c *gee.Context) { names : []string{geektutu} c.String(http.StatusOK, names[100]) }) r.Run(:9999) }得益于第五天的中间件机制错误处理可以直接实现为一个中间件新增文件 gee-web/day7-panic-recover/gee/recovery.gofunc 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并在日志中打印堆栈信息。其中trace()函数通过runtime.Callers(3, pcs[:])获取调用栈的程序计数器跳过前 3 个 Caller即Callers本身、trace、defer func再用runtime.FuncForPC(pc)与fn.FileLine(pc)拿到文件名和行号方便错误定位。框架还提供了Default()构造方法直接内置Logger()与Recovery()两个中间件见 gee-web/day7-panic-recover/gee/gee.go// Default use Logger() Recovery middlewares func Default() *Engine { engine : New() engine.Use(Logger(), Recovery()) return engine }使用示例见 gee-web/day7-panic-recover/main.go访问/正常返回Hello Geektutu访问/panic返回{message:Internal Server Error}之后再次访问/依然正常说明服务没有被 BUG 拖垮后台日志则会打印出runtime error: index out of range及完整的堆栈调用链。运行与验证每个 Day 目录都是独立的 Go 模块可以直接运行验证。例如 Day 3 动态路由# 在 gee-web/day3-router 目录下 go run main.go然后借助 curl 测试$ curl http://localhost:9999/hello/geektutu hello geektutu, youre at /hello/geektutu $ curl http://localhost:9999/assets/css/geektutu.css {filepath:css/geektutu.css}Day 3 的单元测试可以直接执行框架代码在gee/子目录中# 在 gee-web/day3-router/gee 目录下 go test -vDay 5 中间件的效果可以通过日志观察访问/只触发全局Logger访问/v2/hello/geektutu则全局与分组中间件同时生效日志顺序印证了Next()的调用链。Day 6 可在浏览器访问localhost:9999验证模板渲染与 CSS 静态资源加载。Day 7 可通过/panic接口验证错误恢复与堆栈打印。各阶段对应源码位置汇总阶段功能主题源码目录Day 1http.Handler 与静态路由gee-web/day1-http-baseDay 2上下文 Contextgee-web/day2-contextDay 3Trie 树动态路由gee-web/day3-routerDay 4分组控制 Groupgee-web/day4-groupDay 5中间件 Middlewaregee-web/day5-middlewareDay 6HTML 模板与静态资源gee-web/day6-templateDay 7错误恢复 Panic Recovergee-web/day7-panic-recover结语从标准库net/http的Handler接口出发Gee 用七天时间逐步完成了路由表、Context 封装、Trie 树动态路由、分组控制、中间件、模板渲染与错误恢复七个核心能力。这个教程的价值不在于代码量的大小多数阶段仅 50150 行而在于完整呈现了一个 Web 框架的核心设计原则与演进思路先理解标准库的能力边界再逐层补齐框架的价值所在。希望这个教程能够对你有所启发。赞分享示例工程【免费下载链接】7days-golang7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列项目地址https://gitcode.com/gh_mirrors/7d/7days-golang点击查看免费下载相关推荐7天用Go从零实现Web框架Gee教程解析7天用Go从零实现Web框架Gee教程解析 为什么要自己实现Web框架 在Go语言开发Web应用时标准库 net/http 提供了基础功能但实际开发中我们常示例工程七天从零实现 Web 框架 GeeDay1基于 net/http 与 http.Handler 搭建框架雏形七天从零实现 Web 框架 GeeDay1基于 net/http 与 http.Handler 搭建框架雏形 本篇是「7 天用 Go 从零实现 Web 框示例工程七天用Go从零实现Web框架Gee五中间件 Middleware 的设计与实现七天用Go从零实现Web框架Gee五中间件 Middleware 的设计与实现 本文是《7天用Go从零实现Web框架Gee教程系列》的第五篇基于仓库 g示例工程上一篇ImageGPT-small像素级Transformer如何重塑图像生成基础架构下一篇Prepack性能监控告警当优化效果下降时及时通知创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考