最近要做一个 REST API server,在网上搜索了一遍以后,发现常用的是 Gin 和 Echo,并且很多人都说 golang 本身提供的 http server 已经足够强大,gin 和 echo 也只是在外包了一层。
我看 Gin 的源码行数比 Echo 少很多,而且测试覆盖率也高很多,因此决定学习一下 Gin,本文目标有以下这些
- 学习如何设计一个 REST 风格的 server ?
- 学习 Gin 在 go 自带的 http server 基础上做了哪些工作?
启动 Gin http server
在使用 Gin 框架的时候,最后都会调用 gin.Run(":8080")
,这样你的 http server 就可以就收 client 请求了,
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
可见,Run 函数最后调用了 http.ListenAndServe
,所以说 http 协议层的解析等工作都是 go 标准库完成的,Gin 只负责后续针对不同 URL 的路由 (Router) 工作。
http.ListenAndServe
接受一个 Handler 参数,这是一个接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Gin 的 Engine
实现了这个 ServeHTTP
, 所以一个 http request 在 Gin 框架中的处理是从 gin.Engine.ServeHTTP()
开始的, 主要处理逻辑在handleHTTPRequest()
中。
处理请求
既然已经提到了 handleHTTPRequest
函数,那么就来看看它是如何处理 request 的
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
}
}
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
请注意这里的 c.Next()
函数,对 http request 调用我们用户函数进行处理的过程都是在这里完成的,非常隐蔽,如果不注意看根本不会注意到这个 next 函数。
如何证明呢? 用 dlv 打印出 backtrace 即可
0 0x0000000001a64a23 in test-server/internal.(*test-server).helloHandler
at ./internal/server.go:101
1 0x0000000001a67c59 in test-server/internal.(*test-server).helloHandler-fm
at ./internal/server.go:101
2 0x000000000198a49c in github.com/gin-gonic/gin.(*Context).Next
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go:165
3 0x00000000019a8221 in github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/recovery.go:99
4 0x000000000198a49c in github.com/gin-gonic/gin.(*Context).Next
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go:165
5 0x00000000019a6c65 in github.com/gin-gonic/gin.LoggerWithConfig.func1
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/logger.go:241
6 0x000000000198a49c in github.com/gin-gonic/gin.(*Context).Next
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go:165
7 0x00000000019985b7 in github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go:489
8 0x00000000019980c6 in github.com/gin-gonic/gin.(*Engine).ServeHTTP
at /Users/me/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go:445
9 0x00000000015af35f in net/http.serverHandler.ServeHTTP
at /usr/local/go/src/net/http/server.go:2807
10 0x00000000015a9836 in net/http.(*conn).serve
at /usr/local/go/src/net/http/server.go:1895
而且通过观察发现,Next 的调用也是嵌套的。
Gin 的路由功能
前面提到 Gin 实现的是基于 url 的路由功能,那么最重要的就是如何把处理函数注册到对应的 url 上。 Gin 使用了基于 Trie 演变的 Radix Tree 来存放每个 Method 下的 URL,
先看结构体,gin.go 中实现了 Engine
结构体,其中最重要的是 RouterGroup
和 methodTrees
type Engine struct {
RouterGroup
...
trees methodTrees
...
}
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
注释中已经写得很清楚了,这些 handlers 函数也可以被称为是中间件 middleware。
注册全局中间件
一般使用 Default 函数返回一个默认的 gin.Engine 结构,我们看到函数中使用了 Engine.Use
函数把 Logger 和 Recovery 添加到了位于 RouterGroup 的 HandlersChain中去,它的含义就是把 Logger 和 Recovery 作为了全局的中间件。
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
有趣的是如果你在源码中搜索一下 Engine.Use()
,会发现只有初始化一个 Engine 的时候使用了它(当然,用户也可以在自己的代码中调用它),其他都是在 test 文件中,那么问题来了,用户添加 handler 函数的时候,是如何加入进入的呢?
注册路由组 group 中间件
用户注册中间件的时候一般是这样调用的
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
所以,是 handle 函数负责添加 handler,注意其中的 combineHandlers
, 它每次都会把 RouterGroup 注册的全局 handler 也全部 copy 过去。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
而且,handle 是 RouterGroup
的函数, 也就是说,每个 group 都有自己的一套 handlers。
每次在我们用 v1 := engine.Group("/test_group")
创建一个新 group时,也会把 RouterGroup 注册的全局 handler 也全部 copy 过去。
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
总结
到这里尝试回答文章开头的问题:
- 不需要特意实现什么 REST API,做好了 http server 的工作本身就提供了 REST API。 Gin 通过 Group 的方式提供了路由组的改变,可以让用户更好的设计层层递进的 URL 。
- Gin 的实现在上文中已经介绍,还有很多设计思路没有来得及写,有时间可以带着问题再多看看。