Web框架Gee
一. http.Handler(静态路由)
简单的通过封装的http类,通过其解析出来的url生成响应,使用一个string和方法的映射
// 引入 Go 标准库中的包
// 类似于 C++ 中的 #include,导入标准库
import (
"fmt" // 格式化输入输出,类似于 C++ 的 iostream
"log" // 日志记录,类似于 C++ 中的 <fstream> 或 std::cerr
"net/http" // 用于处理 HTTP 请求和响应,C++ 没有直接的标准库实现
)
// 主函数,Go 中程序的入口函数,类似于 C++ 中的 int main()
func main() {
// http.HandleFunc 用于注册 URL 路径与处理函数的映射
// 第一个参数是 URL 路径,第二个参数是对应的处理函数
// 类似于 C++ 中使用回调函数处理特定请求路径
http.HandleFunc("/", indexHandler) // 处理 "/" 路径
http.HandleFunc("/hello", helloHandler) // 处理 "/hello" 路径
// 启动 HTTP 服务器,监听 9999 端口
// log.Fatal 用于记录错误并终止程序,类似于 C++ 中遇到致命错误时的 std::cerr 或 abort()
log.Fatal(http.ListenAndServe(":9999", nil))
// C++ 中没有标准的 ListenAndServe,需要使用第三方库(如 Boost.Asio)实现
}
// indexHandler 处理 "/"" 路径的请求,类似于 C++ 中的回调函数
// w 是 http.ResponseWriter,表示响应的输出流,类似于 C++ 中的 std::ostream
// req 是 *http.Request,表示客户端的请求,类似于 C++ 中封装请求信息的类
func indexHandler(w http.ResponseWriter, req *http.Request) {
// Fprintf 类似于 C++ 中的 printf,但这里输出到 ResponseWriter 而不是控制台
// req.URL.Path 获取请求的 URL 路径,类似于 C++ 中通过类来获取请求的 URL
// C++ 类比:std::cout << "URL.Path = " << req.url_path << std::endl;
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}
// helloHandler 处理 "/hello" 路径的请求
// 类似于 C++ 中为不同的 URL 路径注册不同的处理函数
func helloHandler(w http.ResponseWriter, req *http.Request) {
// Go 的 for range 循环遍历请求头,req.Header 是一个 map
// C++ 类比:可以用 std::map 或 std::unordered_map 来存储请求头信息
// C++ 类比:for (const auto& pair : req.headers)
for k, v := range req.Header {
// 输出每个请求头的键值对,类似于 C++ 中的 cout 或 ostringstream
// Fprintf 写入到 ResponseWriter,相当于输出到 HTTP 响应中
// C++ 类比:std::cout << "Header[" << k << "] = " << v << std::endl;
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
}
代码逐行解释:
-
import
部分:- 引入了 Go 的标准库包,功能类似于 C++ 的
#include
指令。特别是,fmt
提供格式化输出功能,log
提供日志记录,net/http
用于处理 HTTP 请求和响应。C++ 中,处理 HTTP 请求需要使用外部库,如Boost.Beast
或cpp-httplib
。
- 引入了 Go 的标准库包,功能类似于 C++ 的
-
main()
函数:- 这是 Go 程序的入口,类似于 C++ 的
int main()
。在这里,我们使用http.HandleFunc
将 URL 路径与处理函数关联。http.ListenAndServe(":9999", nil)
启动一个监听 9999 端口的 HTTP 服务器,类似于 C++ 使用Boost.Asio
或cpp-httplib
来启动服务器。
- 这是 Go 程序的入口,类似于 C++ 的
-
indexHandler
函数:- 这个函数处理
/
路径的 HTTP 请求。参数w
是 HTTP 响应对象,类似于 C++ 中的std::ostream
,用于向客户端发送响应。req
是 HTTP 请求对象,包含了请求的信息(如 URL 路径)。 fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
将请求路径写入到响应流中,类似于 C++ 中使用std::cout
或std::ostringstream
。
- 这个函数处理
-
helloHandler
函数:- 这个函数处理
/hello
路径的 HTTP 请求。它遍历请求头部req.Header
,并将每个键值对输出到 HTTP 响应中。C++ 中可以通过std::map
或std::unordered_map
迭代键值对,类似于 Go 中的for range
循环。
- 这个函数处理
以下是对这段 Go 代码的详细分析,包括通过 C++ 视角对 Go 的特性和机制进行解释。通过注释解释每个代码块,并着重说明 C++ 中的类比点。
package gee
import (
"fmt"
"log"
"net/http"
)
- 解释:导入标准库包:
fmt
:用于格式化输出,类似于 C++ 中的iostream
或printf
。log
:用于记录日志,类似于 C++ 中使用std::cerr
或文件日志库。net/http
:Go 内置的 HTTP 库,处理 HTTP 请求和响应。C++ 没有标准库,需要借助Boost.Beast
或cpp-httplib
等第三方库。
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)
- 解释:定义了一个类型
HandlerFunc
,它是一个函数类型,参数是http.ResponseWriter
和*http.Request
。http.ResponseWriter
:类似于 C++ 中用于输出响应的std::ostream
。*http.Request
:表示 HTTP 请求,类似于 C++ 中封装请求信息的类。
// Engine implement the interface of ServeHTTP
type Engine struct {
router map[string]HandlerFunc
}
- 解释:定义了
Engine
结构体,它包含一个路由表router
,这个表通过map[string]HandlerFunc
映射 URL 到处理函数。- 类似于 C++ 中将 URL 路径映射到函数指针或回调函数的机制,可以用
std::map<std::string, std::function<void(...)>>
实现。
- 类似于 C++ 中将 URL 路径映射到函数指针或回调函数的机制,可以用
// New is the constructor of gee.Engine
func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
- 解释:构造函数
New
返回一个指向Engine
实例的指针。- 类似于 C++ 中的构造函数
Engine::Engine()
,并返回new Engine()
。
- 类似于 C++ 中的构造函数
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
log.Printf("Route %4s - %s", method, pattern)
engine.router[key] = handler
}
- 解释:
addRoute
方法用于向router
映射表中添加路由。key
由请求方法(GET
/POST
)和 URL 模式组成。- C++ 中可以用类似
std::string key = method + "-" + pattern; router[key] = handler;
来实现。
- C++ 中可以用类似
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
- 解释:
GET
方法用于注册 GET 请求,封装了addRoute
方法。对应 C++ 中类似于类的成员函数封装。
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
- 解释:
POST
方法用于注册 POST 请求,类似于GET
方法,只是请求方法不同。
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
- 解释:
Run
方法启动 HTTP 服务器并监听指定地址(addr
)。- C++ 中可以使用
Boost.Asio
等库启动服务器。http.ListenAndServe
是 Go 的简化实现。
- C++ 中可以使用
// ServeHTTP implements the http.Handler interface
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
- 解释:
ServeHTTP
方法实现了http.Handler
接口,用于处理传入的 HTTP 请求。key := req.Method + "-" + req.URL.Path
:生成键,用于匹配路由表中的处理函数。if handler, ok := engine.router[key]; ok
:检查路由表中是否存在对应的处理函数。如果存在,则调用对应的处理函数;否则返回 404 错误。- C++ 类比:可以通过
std::map::find
查找键并调用对应的函数指针或回调函数。
func main() {
r := gee.New() // 创建一个新的 Engine 实例
// 注册 "/" 路径的 GET 请求处理函数
r.GET("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
// 注册 "/hello" 路径的 GET 请求处理函数
r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})
// 运行服务器,监听 9999 端口
r.Run(":9999")
}
二. 上下文封装
封装上下文,用于处理请求报文和生成对应格式的响应报文
package gee
import (
"encoding/json" // 用于编码和解码 JSON 数据
"fmt" // 用于格式化字符串输出
"net/http" // Go 标准库,提供 HTTP 请求和响应处理
)
// H 是 map[string]interface{} 的别名,通常用于返回 JSON 数据
type H map[string]interface{}
// Context 结构体封装了 HTTP 请求和响应的信息,简化了 HTTP 操作
type Context struct {
// origin objects
Writer http.ResponseWriter // HTTP 响应写入器,用于写入 HTTP 响应
Req *http.Request // HTTP 请求,包含请求的相关信息
// request info
Path string // 请求的 URL 路径
Method string // HTTP 请求方法,如 GET、POST
// response info
StatusCode int // HTTP 响应的状态码
}
// newContext 是 Context 的构造函数,初始化并返回一个 Context 实例
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Writer: w, // 初始化 HTTP 响应写入器
Req: req, // 初始化 HTTP 请求对象
Path: req.URL.Path, // 从请求中提取路径
Method: req.Method, // 从请求中提取 HTTP 方法
}
}
// PostForm 用于获取 POST 请求的表单参数
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key) // 从 POST 表单中获取参数
}
// Query 用于获取 URL 中的查询参数
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key) // 从 URL 查询字符串中获取参数
}
// Status 设置 HTTP 响应的状态码
func (c *Context) Status(code int) {
c.StatusCode = code // 设置状态码
c.Writer.WriteHeader(code) // 将状态码写入 HTTP 响应头
}
// SetHeader 设置 HTTP 响应头
func (c *Context) SetHeader(key string, value string) {
c.Writer.Header().Set(key, value) // 设置指定的响应头
}
// String 返回普通文本响应
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 返回 JSON 格式的响应
func (c *Context) JSON(code int, obj interface{}) {
c.SetHeader("Content-Type", "application/json") // 设置响应头为 JSON 类型
c.Status(code) // 设置响应状态码
encoder := json.NewEncoder(c.Writer) // 创建 JSON 编码器
if err := encoder.Encode(obj); err != nil { // 将对象编码为 JSON 并写入响应体
http.Error(c.Writer, err.Error(), 500) // 如果编码失败,返回 500 错误
}
}
// Data 返回原始字节流数据
func (c *Context) Data(code int, data []byte) {
c.Status(code) // 设置响应状态码
c.Writer.Write(data) // 将字节数据写入响应体
}
// HTML 返回 HTML 格式的响应
func (c *Context) HTML(code int, html string) {
c.SetHeader("Content-Type", "text/html") // 设置响应头为 HTML 类型
c.Status(code) // 设置响应状态码
c.Writer.Write([]byte(html)) // 将 HTML 字符串写入响应体
}
代码功能梳理与知识点解析
1. 封装 HTTP 请求与响应
Context
是一个对 HTTP 请求和响应的封装,简化了对请求信息的读取和响应信息的生成。Writer
是 Go 的http.ResponseWriter
,用于写入 HTTP 响应;Req
是http.Request
,包含了 HTTP 请求的所有信息,如路径、方法、请求体等。
2. 获取请求数据
PostForm(key string)
:用于获取 POST 请求的表单数据。类似于 HTML 表单提交时,从请求体中提取参数。Query(key string)
:用于从 URL 中提取查询参数(通常在 GET 请求中使用)。
3. 设置响应状态与头部信息
Status(code int)
:设置 HTTP 响应的状态码,例如 200(成功)、404(未找到)等。SetHeader(key string, value string)
:设置响应的 HTTP 头部信息,通常用于设置响应的内容类型(Content-Type
)或其他元数据(如缓存策略等)。
4. 返回不同格式的响应
String(code int, format string, values ...interface{})
:返回一个普通的文本响应,内容是通过fmt.Sprintf
格式化的字符串。JSON(code int, obj interface{})
:返回 JSON 格式的数据响应,将obj
编码为 JSON 并写入响应体,常用于 REST API。Data(code int, data []byte)
:返回原始的字节数据,适用于返回文件、图片或其他二进制数据。HTML(code int, html string)
:返回 HTML 格式的响应,用于返回网页内容。
知识点总结
-
HTTP 请求的解析
- 通过
Context
封装的Req
对象,开发者可以轻松地从 HTTP 请求中提取表单数据(PostForm
)和查询参数(Query
)。 - 在 HTTP 请求中,表单数据存储在请求体中(POST 请求),而查询参数存储在 URL 中(GET 请求)。
- 通过
-
HTTP 响应的生成
Context
提供了多种方法来生成响应,包括文本(String
)、JSON(JSON
)、HTML(HTML
)等格式的响应。通过封装这些方法,开发者无需手动编写响应头和响应体的构建逻辑。
-
状态码与头部信息
- 状态码通过
Status
方法进行设置,表示请求的处理结果,如200 OK
、404 Not Found
等。 - 响应头通过
SetHeader
方法进行设置,用于指定响应内容的格式、缓存策略等。
- 状态码通过
-
JSON 编码与解码
- 通过 Go 的标准库
encoding/json
,Context
可以将任意对象编码为 JSON 格式并写入响应中。 - 这在 RESTful API 开发中尤为常见,服务器通常以 JSON 格式返回数据供客户端消费。
- 通过 Go 的标准库
三. 前缀树动态路由解析
HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。
我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。
接下来我们实现的动态路由具备以下两个功能。
参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc 和 /p/go/doc。
通配。例如 /static/filepath,可以匹配/static/fav.ico,也可以匹配/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径。
这个代码实现了一个基于 前缀树(Trie) 的路由管理器,它的主要功能是支持基于 URL 路径的路由匹配。路由中可以包含静态路径和动态路径(以 :
或 *
开头)。这种实现方式非常适合用来做动态路由匹配,常见于 Web 框架中的路由管理。
1. 结构体 node
node
是前缀树的节点结构,每个节点代表路径中的一部分。它有以下几个字段:
pattern string
:完整的路由模式(如/p/:lang/doc
)。如果该节点是一个终端节点,它会存储这个完整的路由路径。否则为空字符串。part string
:该节点代表路径中的一部分,比如:lang
或doc
,每个节点只存储路径中的一个部分。children []*node
:节点的子节点,表示树中的下一层路径部分。isWild bool
:标记路径中的动态部分(通配符部分),如果part
以:
或*
开头,isWild
会被设置为true
。例如,:lang
或*filepath
都属于通配符。
type node struct {
pattern string
part string
children []*node
isWild bool
}
2. 节点的字符串表示方法 String()
String()
方法是 node
的字符串格式化函数,方便调试时输出节点信息。它输出 pattern
、part
和 isWild
的值。
func (n *node) String() string {
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
3. insert()
方法:插入节点
insert()
方法用于将新的路由模式插入到前缀树中。该方法会根据路由路径的分段递归地插入各个部分。
-
参数说明:
pattern string
:表示完整的路由模式,比如/p/:lang/doc
。parts []string
:路径部分的切片,比如对于/p/:lang/doc
,parts
切片为["p", ":lang", "doc"]
。height int
:当前递归的深度,表示处理到路径的第几部分。
-
执行流程:
- 如果
parts
的长度和height
相等,说明已经处理完了所有路径部分,设置该节点的pattern
为完整路径。 - 否则根据当前的路径部分
part
,找到或创建对应的子节点,并递归插入剩下的部分。
- 如果
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)
}
4. search()
方法:查找节点
search()
方法用于在前缀树中查找与指定路径匹配的节点。
-
参数说明:
parts []string
:路径分段的切片。height int
:递归的深度,表示处理到路径的第几部分。
-
执行流程:
- 如果已经到达路径的最后一部分或者当前节点是
*
通配符,检查当前节点是否有pattern
,如果有则返回。 - 否则,根据当前部分
part
找到所有可能的子节点,并递归地进行查找。 - 返回第一个找到匹配的节点。
- 如果已经到达路径的最后一部分或者当前节点是
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
}
5. travel()
方法:遍历树节点
travel()
方法用于遍历整个前缀树,将每个含有完整路由模式的节点添加到列表中。
-
参数说明:
list *[]*node
:指向一个存储节点的切片的指针。
-
执行流程:
- 如果当前节点含有
pattern
,将该节点添加到列表中。 - 递归调用子节点的
travel()
方法,继续遍历子节点。
- 如果当前节点含有
func (n *node) travel(list *([]*node)) {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
child.travel(list)
}
}
6. matchChild()
方法:匹配单个子节点
matchChild()
方法用于匹配当前节点的子节点中第一个与传入的路径部分匹配的节点。
- 匹配规则:如果当前路径部分等于
part
或当前子节点是通配符节点(isWild
为true
),则匹配成功并返回该节点。
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
7. matchChildren()
方法:匹配多个子节点
matchChildren()
方法用于找到当前节点的所有匹配的子节点,返回一个包含匹配节点的切片。
- 匹配规则:如果路径部分等于子节点的
part
,或者子节点是通配符节点,则将其添加到返回的列表中。
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
8. 代码功能总结
这个代码实现了一个简化的基于前缀树的动态路由管理器,功能如下:
- 动态路由匹配:支持静态路由和动态路由(如
:id
和*
路径)。 - 插入路由:通过
insert()
方法,将路由路径分解并插入到树结构中。 - 查找路由:通过
search()
方法,递归查找树中的匹配节点。 - 遍历路由:通过
travel()
方法,遍历所有具有完整路径的节点。
四. 分组控制
实际上就是由分组来接管注册路由的功能,但分组还是通过间接调用engine来实现操作
不过在使用分组方法的时候,可以根据该分组前缀进行拼接,方便不同分组的快速注册,同时实现嵌套分组的功能,也保证了engine操作统一
大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。
中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。
一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如/,或者/api;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;
当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。
RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
可以仔细观察下addRoute函数,调用了group.engine.router.addRoute来实现了路由的映射。
由于Engine从某种意义上继承了RouterGroup的所有属性和方法,因为 (*Engine).engine 是指向自己的。这样实现,我们既可以像原来一样添加路由,也可以通过分组添加路由。
func New() *Engine {
engine := &Engine{router: newRouter()}
engine.RouterGroup = &RouterGroup{engine: engine}
engine.groups = []*RouterGroup{engine.RouterGroup}
return engine
}
// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
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 defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
group.addRoute("POST", pattern, handler)
}
五. 中间件
给每一个分组注册相应的中间件,也就是待执行的处理函数,上下文会根据url前缀解析获得嵌套的分组以及对应的微服务,同时获取路由转发的函数
然后统一使用next进行执行
中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。
因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:
插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。
如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
engine.router.handle(c)
}
六. 静态文件和模版
静态文件请求其实就是注册一个GET以及相应获取文件的方法,处理逻辑就是路由映射处理函数,再映射获取相应文件地址,再调用相应处理函数