用go实现一个web应用是十分方便的,标准库为我们提供了方便的api
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello") }) http.ListenAndServe(":9999", nil)}
三行代码就能实现一个基础的web应用的功能,那我们为什么还需要gin这样的框架呢
首先说下相较于标准库不能满足需求的地方
路由算法简陋,且不支持动态路由
http.HandleFunc
注册路由时,调用的是DefaultServeMux.HandleFunc
,他是一个ServerMux
类型的对象,结构如下
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames}type muxEntry struct { h Handler pattern string}
由结构体不难猜出他的大概思路,用一个map存储路由和Handler
的映射,并且用一个slice维护这些Entry
,看一下他注册路由和匹配路由的源码
func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() //使用的是普通的map,有并发的危险,手动上一个RWlock 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) //基本上就是一个简单的插入操作,但要维护这个slice有序 } if pattern[0] != '/' { //如果开头不是‘/’,前面的就是host部分 mux.hosts = true }}
// Find a handler on a handler map given a path string.// Most-specific (longest) pattern wins.func (mux *ServeMux) match(path string) (h Handler, pattern string) { // Check for exact match first. v, ok := mux.m[path] if ok { return v.h, v.pattern } // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, ""}
看到这里就知道他为什么还要再维护一个有序的slice了,为了实现模糊匹配,正向遍历这个slice就能得到"Most-specific"的那个路由,但是有一点,如果你注册的路由不是以"/"结尾,那么就不会在这个slice中,也就不能实现模糊匹配。
标准库的路由算法虽然简单明了,但是不支持动态路由,而且使用这样的slice作为路由匹配的媒介效率并不是很高(虽然也很够用了)。
不支持中间件
这里的中间件指的是狭义的中间件,在每一次HTTP请求和响应中,我们总要做一些固定的事情,比如:认证授权,跨域设置,参数校验等等。我们希望能用中间件来统一 管理这些事情,从而在handler中专注于当前接口要做的事情。当然有中间件就需要路由分组,不然就太不灵活了。
错误恢复
对于一个web应用来说,一个错误就导致服务停掉是不行的,gin中把错误恢复作为一个中间件实现。
以上三点是web应用开发中不可以没有的点,当然gin还做了很多其他的事情,例如context的封装,模板渲染,常用api的封装等等。但以上三点是我选择使用gin(或其他框架)的主要原因。
然后再看一下gin是怎么为我做的
func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { c.String(200, "Hello") }) r.Run(":9999")}
使用gin后对于代码结构几乎没有什么改变
显式的使用engine这个对象,使用它调用api
将request,responseWriter和其他上下文相关信息封装到了context中
可以看出gin其实是很轻量的框架。
路由
gin中的路由算法使用的是radix tree也就是前缀树。前缀树的优点十分明显。
相比在一个slice中顺序查找,匹配路由的开销更小
由于前缀树的结构,可以很容易的实现对动态路由的支持
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) // Update maxParams if paramsCount := countParams(path); paramsCount > engine.maxParams { engine.maxParams = paramsCount } if sectionsCount := countSections(path); sectionsCount > engine.maxSections { engine.maxSections = sectionsCount }}
这是gin中添加路由的方法,gin中按照http请求方法的不同(GET,POST...)分为不同的几棵树,每颗树分别管理。
在匹配路由时就是根据前缀树匹配,这块的源码有些复杂就不展示了,基本上就是按照常规的前缀树算法递归的寻找,其中每个节点是按路径中的/分割的。同时为了支持动态路由加入了一些:
和*
的判断。
中间件
我认为gin的中间件的设计是十分巧妙的,他大概遵从一个这样的模型
中间件负责在每次请求到达用户的handleFunc前做某些事情,在用户的handleFunc执行后再做某些事情,其中有Next方法来控制哪些事情再前面做,哪些事情再后面做。
相当于是一个流水线,流转方向是 req-->handleFunc-->resp,中间件就是在handleFunc两侧的工人
路由组
但是这个模型有一个很大的问题,可以看到所有的请求都会经过middleware,但是我们实际开发中的需求往往是相当复杂的,我希望某些接口需要认证才能访问,有些则不需要,那么我们就需要路由组了,这部分就不详细介绍了,只需要知道engine内嵌套了routerGroup,所以engine有routerGroup所有的行为,而routerGroup中也持有engine的指针,也能知道自己的父亲是谁。
下面讲一下gin是如何处理中间件的
中间件也是HandleFunc
根据这个图,所谓的中间件其实也是对request和response做某些操作,所以他和HandleFunc(context)其实没什么区别,gin中的中间件也是HandleFunc类型的
// 使用Use注册中间件r.Use(func(c *gin.Context) { fmt.Println("hi")})
看一下中间件是如何应用的
// Use adds middleware to the group, see example code in GitHub.func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj()}
相当简单。每个中间件就是一个handler,在访问用户路由的时候,让请求依次走过这些Handler,到达用户的HandleFunc,再让响应依次经过这些中间件,就实现了上面的流水线。
在每次请求到来时,会为这次请求创建一个context,并且为这个context初始化handlers数组,这个数组里面就是所有在此路由组上的中间件,然后再将路由匹配后的handler append到数组里,流水线就组装好了!
Next()
之前提过,中间件需要再请求前或请求后都做一些事情,靠的就是这个Next()
故名思义就是执行下一个中间件
func (c *Context) Next() { c.index++ // 之所以需要遍历去执行是因为不是所有的handler都会调用Next() // 调用Next()是为了控制在请求前后各实现一些行为 // 如果只作用在请求前,可以省略调用Next() for c.index < len(c.handlers) { // 因为是先执行,后index++,所以能保证每一个handler都会被执行,不会跳过 // 并且由于之前调用过index++,所以没有调用Next()的handler也会执行 // 最后一个handler是实际请求的handler,这个handler中不会有Next() // 所以执行完这个handler后,index还是len(handlers)-1 // 此时index++后退出循环,返回 c.handlers[c.index](c) c.index++ }}
了解了Next()之后,我们就可以自己实现任何想要的中间件了
Abort()
这个方法用于直接跳过接下来的中间件,也就是将index置于要结束的位置,中间件就不再执行下去了,例如用于认证用户身份的中间件,如果认证失败,可以直接调用这个方法。
错误恢复
gin中默认为我们实现了两个中间件,并且在gin.Default()
中应用了,分别是日志和错误恢复。
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc { if len(recovery) > 0 { return CustomRecoveryWithWriter(out, recovery[0]) } return CustomRecoveryWithWriter(out, defaultHandleRecovery)}// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { var logger *log.Logger if out != nil { logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) } return func(c *Context) { defer func() { if err := recover(); err != nil { // Check for a broken connection, as it is not really a // condition that warrants a panic stack trace. var brokenPipe bool if ne, ok := err.(*net.OpError); ok { if se, ok := ne.Err.(*os.SyscallError); ok { if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { brokenPipe = true } } } if logger != nil { stack := stack(3) httpRequest, _ := httputil.DumpRequest(c.Request, false) headers := strings.Split(string(httpRequest), "\r\n") for idx, header := range headers { current := strings.Split(header, ":") if current[0] == "Authorization" { headers[idx] = current[0] + ": *" } } headersToStr := strings.Join(headers, "\r\n") if brokenPipe { logger.Printf("%s\n%s%s", err, headersToStr, reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), headersToStr, err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) } } if brokenPipe { // If the connection is dead, we can't write a status to it. c.Error(err.(error)) // nolint: errcheck c.Abort() } else { handle(c, err) } } }() c.Next() }}func defaultHandleRecovery(c *Context, err interface{}) { c.AbortWithStatus(http.StatusInternalServerError)}
这个错误恢复还是有些复杂的。总而言之我们可以在这个错误恢复的基础之上自定义一些函数交给他去执行。其实我需要的功能没有那么多,只需要打印一下堆栈信息然后返回一个状态码为500的响应,然后recover就可以了。
写到这里去官网看了一眼,发现我写的这三个方面恰好是他官网宣传的前三个...
当然他还宣传了其他功能,像是JSON的验证(JSON相关的库就太多了),模版渲染(基本上用不到)等等
结语
这篇文章主要讲了一下我用gin所用到的几个功能,只是对这些功能的一个介绍,其实gin更核心的部分在于context和engine的封装,可能需要再研究一阵子才能写一篇文章。
总的来说gin的使用体验十分良好,不过文档实在简陋,甚至没有详细的reference,好在源码十分友好,易读性可以并且注释很好读(或许这就是go doc的理念吧)。
原文:https://juejin.cn/post/7100824610307833863