〇、杂谈
前几天面了腾讯的暑期实习,被狠狠薄纱了,这几天也有些低落。但是怎么办呢,谁让大学玩了两年多。学golang学了一个多月了,这几天也比较详细地学习了Gin框架,这篇文章会在geektutu的《7天用Go从零实现Web框架Gee教程》(原地址:7天用Go从零实现Web框架Gee教程 | 极客兔兔)和小徐先生1212的《gin框架底层技术原理剖析》(原地址:gin框架底层技术原理剖析_哔哩哔哩_bilibili)的基础上详细展现Gin框架的原理。
一、为什么要用Gin框架?
1、更加简便的中间件使用
在原http包中使用一个中间件的代码往往要如下:
func Middleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Middleware before")
next.ServeHTTP(w, r)
fmt.Println("Middleware after")
}
}
func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Println("hello")
}
func main() {
http.Handle("/", Middleware(Hello))
err := http.ListenAndServe(":8080", nil)
if err != nil {
return
}
}
可能一层中间件看着还好,可是如果很多层呢?而在gin框架中将所有要执行的路由处理函数封装到了context上下文中的路由处理函数组之中了,就无需程序员针对一个又一个路由去衔接。
2、提供了动态路由,易于维护
在http包中的ServeMux路由处理器只提供了静态路由的映射,担当网络过于复杂时会造成过大的管理负担,而动态路由则更灵活且容易维护。在Gin中由前缀树的方式实现动态路由。
3、提供了路由分组,方便管理
通过路由分组的方式进一步方便了路由的管理。
二、HTTP包简介(服务器端)
http的使用是非常简单的,只需要一点代码即可,:
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
http.ListenAndServe(":8080", nil)
}
首先解释这里的http.HandleFunc,这是http中的核心字段,Handle是一个接口,实现了
ServeHTTP(ResponseWriter, *Request)
方法(这个方法的底层实现就是调用自己,即一个路由处理函数的servehttp就是这个执行这个路由处理函数本身的逻辑)。而这一点代码其实就可以体现http包的本质就是不断的注册路由和路由处理函数的映射关系,从而当一个url到来时对其进行处理。
那么http包的整体运行逻辑是怎么样的呢?(这只是一个大致流程,如果对具体代码感兴趣可以看小徐先生的编程世界中的Golang HTTP 标准库实现原理)
首先是服务器,里面封装了ip地址端口号和一个路由处理器,如果这里使用默认的路由处理器,那么也就是上面的http.ListenAndServe(":8080", nil)所传入的nil。
然后根据传入的端口申请到一个监听器 listener来持续监听。
之后通过监听器循环判断是否有连接到来,如果有就创建一个goroutine去处理这个连接。处理的方式就是去在路由处理器中匹配url对应的路由处理函数,然后执行。
这里我给表述成了一个非常简单的三步走的样态,而Gin框架就是在这个流程上去进行进一步的封装。
三、Gin框架做了什么?
1、封装服务器与替代默认路由处理器
在http的处理逻辑中,核心的就是从url匹配到路由处理函数(handler)的过程。可这里的映射是一个静态的映射,但是我们要实现动态路由和其他的功能,那就必须把三步走里面的最后一步给切断,那么这里是怎么实现的呢?
这里查询http包我们可以看到
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
在服务器(server)的serverhttp中会执行handler的serverhttp,那么这个handler是什么呢?进一步查阅会看到。
Handler Handler // handler to invoke, http.DefaultServeMux if nil
原来就是我们之前传入的nil,如果是nil,这里执行的就是默认路由处理器(defaultservemux)的serverhttp。而同时显然这里的服务器(server)的servehttp并没有进行匹配的操作,那么我们继续往下查阅
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
果然是在默认路由处理器(defaultservemux)这里进行的匹配操作。这样我们就找到下手的地方了,我们就用我们的engine来替代这里的默认路由处理器就可以实现我们希望的路由匹配逻辑了。那我们就用engine来替换原来的默认路由处理器(defaultservemux),可是这样也会有些麻烦,就是我们监听的时候要用:http.ListenAndServe(":8080", engine)。那我们为什么不把这里也封装起来呢?
所以我们现在明晰了第一个数据结构engine,他来做两件事,第一个是替代原本的默认路由处理器(defaultservemux),第二个是封装原来的服务器(server)。所以我们的engine里面首先给他创建一个router去进行路由处理函数的匹配,router里面是一个url和路由处理函数的map。
2、封装*http.Request请求与http.ResponseWriter响应
每次操作都传这两个东西写的不累吗?封装!我们使用GO语言原本的上下文思想,通过context来封装这两个东西。这是我们的第三个数据结构context。
3、前缀树路由
在geektutu的gee框架中,这里完成是一个前缀树,但实际上在gin框架中是一个压缩前缀树。主要优化了空间的节省,即如果某个子节点是其父节点的唯一孩子,则与父节点进行合并。
那么这个前缀树的操作我们放到哪里呢?显然放到engine的router里面去,这样当我们注册一个函数的时候,就调用engine的router下的方法去给树加上节点,同时给map里面添加对应的映射。而且我们知道http协议中有很多不同的方法,那么显然,我们可以给这些不同方法构造不同的树,所以方法和前缀树之间也是一个map。具体的可以看开头提及的两篇文章。
4、路由组
在http包当路由过多时,我们希望把功能相同的路由通过相同的前缀统一管理起来,比如登陆注册更新删除这些对用户的操作我们希望都用/user/前缀统一,但可惜只能通过注释来进行。但是现在我们可以通过路由组来实现。
我们希望一群有相同前缀的路由可以被管辖在同一个组的数据结构中。那么首先engine就是一个最大的路由组,统辖着所有的路由。 那么现在路由组要做些什么呢?首先要能创建一个子组吧,然后我们之前是在路由器的基础上去注册路由,现在我们可以在路由组的基础上来进行路由的注册了,注册的时候自动加上前缀就可以了。然后我们就可以像树一样来构建我们的路由了。具体的路由组和前缀树和engine和router协调的代码可以到两个文章中看。
5、中间件
接下来是最重要的中间件了,前文中也介绍了在http中使用中间件是一件很麻烦的事情,我们如何让这件事情变的简单呢?
首先的切入点是路由组,我们在使用中间件时并不是对所有操作都进行某一个中间件,而是想要对对应的路由处理函数使用对应的中间件。所以我们首先在路由组中加一个路由处理函数组(一个中间件也是一个路由处理函数),里面存放我们希望这个路由组都执行的中间件。然后在最后执行的时候把他们全部都调用,最后执行我们的路由处理函数。
可是这样还有一个问题,我们在调用的时候并不希望是一个一个的调用,而是链式的调用,中间件是可以在我们的路由处理函数结束之后结束的。那怎么做到呢?我们有现成的上下文来实现这件事情,我们可以在上下文中也添加这样的路由处理函数组,当我们遍历的时候把所有的函数都添加到context的路由处理函数组中,随后通过一个方法去按顺序执行其中的函数,而在中间件中我们可以人为的使用next方法去调用下一个context中的路由处理函数。这样就实现了链式调用。