🐀 Go | Golang Web & Gin部分源码分析

Go 是一种十分适合 Web 开发的语言,语法简单并且容易上手,同时内置go这个并发的神器语法,使得我们可以轻易的开发一些多并发的项目,用在并发需求数量巨大的服务端是再合适不过了。

上个学期曾经使用过 go 写一个实时同步的 MOBA 游戏的服务端,通过go的并发特性很容易就做到了高并发,支持大量的用户同时在线。同时,Golang 在网络方面的基础库也是相对地完善,因此写一个 Http 框架的复杂度也不会更高。

awesome-go整理了一些用 go 语言编写的比较好的库,其中单单Web Frameworks部分就有 37 个。有Beego这种大而全的传统 MVC 架构的框架,也有echogin这种微框架。之前开发过的两个项目都是使用Iris这个 Web 框架的,同样也是一个重型框架,不过这里来看一个微框架gin

gin是一个 golang 的 Http 微框架,有着不错的性能以及封装优雅的 API,提供了路由,中间件,上下文等机制,非常使用写一些微服务,而如今 go 语言在这一方面也有着比较高的使用率,国内的很多互联网公司都把一些微服务用 go 语言重写了。

简单的 Web 程序

这里先来写一个简单的 Cloud 程序

项目架构

1
2
3
4
5
6
.
├── controller
│ └── controller.go
├── model
│ └── model.go
└── main.go

Router

这里简单地实现了四个 API

  • GET /list,列出内容
  • POST /add,添加内容
  • GET /test,输出一段简单的字符串
  • GET /date,输出当前时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"github.com/ZhenlyChen/cloudgo/controller"
"github.com/gin-gonic/gin"
)

func main() {
// gin.SetMode(gin.ReleaseMode) // 生产模式
r := gin.Default()
// 设置路由
r.GET("/list", controller.Get)
r.POST("/add", controller.Add)
r.GET("/test", controller.Test)
r.GET("/date", controller.Date)
// 监听 127.0.0.1:3000
r.Run(":3000")
}

curl

使用curl对服务端一些端口进行访问

1541999214417

AB 测试

使用ab对服务器进行压力测试

  • -n 请求的总数
  • -c 并发的客户端个数

1542023169301

这里一共发送了 10000 个请求,一共耗费了 3.730s,当然这个和机器的性能也有一定的关系。

Gin 源码分析

这里就来分析一下 Gin 的源码和实现。

首先,来看看官方网站的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Quick Start
package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

这里创建了一个默认的gin实例,在/ping下挂上一个简单的路由,返回一个简单的 JSON

gin.Default

首先来看看gin.Default()

1
2
3
4
5
6
7
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault() // 输出调试信息
engine := New() // 新建实例
engine.Use(Logger(), Recovery()) // 添加中间件
return engine
}

首先,调用debugPrintWARNINGDefault输出调试信息,这里调试信息统一使用debugPrint进行输出

1
2
3
4
5
func debugPrint(format string, values ...interface{}) {
if IsDebugging() {
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
}
}

通过控制运行模式debugrelease,就可以统一控制调试信息的输出,这样的设计对于日志输出管理无疑带来了好处。

New

这里通过New()方法来产生一个gin的实例,这实际上也是 go 语言中一个比较常见的用法。因为 go 语言并没有类的概念,其他语言中的类,go 可以通过使用结构体然后再上面绑定一些方法来实现同样功能,然而结构体并没有构造函数。因此,在 go 语言中,通常在一个包中使用New()表示这个包生产一个实例的方法,这个方法会返回一个实例的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { // 初始化临时对象池
return engine.allocateContext()
}
return engine
}

这里使用默认参数初始化了一个Engine结构体,然后为一些成员如RouterGroup,FuncMap初始化。

由于一些成员还需要父类(虽然不应该叫做类,但是不知道叫什么好)的引用,就需要在初始化之后把自身的指针交给子成员,这里RouterGroup需要持有Engine的指针,因此就需要在这个时候给他。

Middleware

然后engine.Use(Logger(), Recovery())中的Use明显就是 Http 路由中的中间件的用法,使得所有的请求都会经过这个中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) // 添加中间件
engine.rebuild404Handlers() // 重建404错误的Handlers
engine.rebuild405Handlers()
return engine
}

// 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()
}

engine.RouterGroup.Use这个方法将中间件函数使用append加入当前路由组(根路由)的调用链下,在 Gin 的设计中,HandlersChain是一个经常出现的东西,也是一个Http Router的核心组成,其本质就是一个函数数组,当路由匹配的时候,就会调用当前路由依次调用这个数组中绑定的方法和中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

// 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
}

添加了中间件之后,这个路由中的 404 和 405 对应的Handler也要及时地更新,加入中间件。

绑定之后Use函数返回了一个IRoute的接口,当实际上返回的是一个RouterGroup对象,通过IRoute,可以过滤掉一些函数,只返回允许外部调用的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// IRoutes defines all router handle interface.
type IRoutes interface {
Use(...HandlerFunc) IRoutes

Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes

StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}

绑定了中间件后,函数会返回一个路由组,这时候,我们还可以这个路由组下无限嵌套绑定子路由。

可以看到,这里绑定的中间件为LoggerRecovery,前者负责日志,后者负责从恐慌中恢复。

Logger

Logger是一个负责日志的中间件,他返回的正是一个HandlerFunc,也正是上面提到的HandlersChain的组成,Use将其加入到根路由的HandlersChain中,就可以实现任意路由的访问之前先调用。

1
2
3
4
5
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc {
return LoggerWithWriter(DefaultWriter)
}

我们再来看看他返回的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
// Example: os.Stdout, a file opened in write mode, a socket...
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
isTerm := true

if w, ok := out.(*os.File); !ok ||
(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) ||
disableColor { // 判断是否在终端模式中
isTerm = false
}

var skip map[string]struct{} // 不需要输出日志的路由

if length := len(notlogged); length > 0 {
skip = make(map[string]struct{}, length)

for _, path := range notlogged {
skip[path] = struct{}{}
}
}

return func(c *Context) {
// Start timer
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery

// Process request
c.Next()

// Log only when path is not being skipped
if _, ok := skip[path]; !ok {
// Stop timer
end := time.Now()
latency := end.Sub(start)

clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
var statusColor, methodColor, resetColor string
if isTerm {
statusColor = colorForStatus(statusCode)
methodColor = colorForMethod(method)
resetColor = reset
}
comment := c.Errors.ByType(ErrorTypePrivate).String()

if raw != "" {
path = path + "?" + raw
}

fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, resetColor,
latency,
clientIP,
methodColor, method, resetColor,
path,
comment,
) // 输出日志信息
}
}
}

这个函数稍微有点长。

首先,他使用了go-isatty这个库,来获取当前的系统运行环境,从而判断是否在终端里面运行

然后使用了一个 map 来存储一些不需要日志的路由

接下来就是返回了一个Handler函数

当接受到一个请求的时候,记录下当前的时间,请求路由和请求数据,然后调用c.Next()

这里就不得不提一下 Web 框架的一个比较基本的结构了。

在一般的 Web 框架里面,通常使用的是一种叫做洋葱圈的结构,因为这种结构的调用机制十分类似洋葱,一层层进入然后一层层离开,有点类似下面的结构。

img

之前使用Node.js的时候,koa框架就是典型的洋葱圈结构,而另一个比较出名的框架express,用的是一种尾递归的方式实现。

1
2
3
4
5
6
7
8
9
// 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 s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c) // 依次执行下一个路由
}
}

当请求进入这个中间件之后,这个c.Next()表示进入下一层的中间件,等到里层的中间件全部进入并执行完毕退出的时候,程序的执行又会回到当前中间件来。

如果某个中间件中不执行c.Next()就意味着当前请求不再返回函数体中,而是依次执行。

这种结构就十分适合做日志处理,一般日志都是需要等待处理结果出来之后才能输出的,同时也要记录当前执行所消耗的时间。使用洋葱圈模型处理中间件,就正好可以实现。当请求来到的时候,记录当前时间,然后交给下一步处理,当处理完成的时候,又返回来,结合当前的上下文和时间,得到处理结果和耗时,就正好可以输出。这种模型天生就适合于 Web 后端对于请求的处理。

Recovery

可以收集所有的错误,这是gin的一大特性,Recovery这个中间件明显就是负责从恐慌中恢复并处理错误的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 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) 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 se.Err == syscall.EPIPE || se.Err == syscall.ECONNRESET {
brokenPipe = true
}
}
}
if logger != nil {
stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httprequest), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), string(httprequest), err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}

// If the connection is dead, we can't write a status to it.
if brokenPipe {
c.Error(err.(error))
c.Abort()
} else {
c.AbortWithStatus(http.StatusInternalServerError)
}
}
}()
c.Next()
}
}

他这里主要使用到一个内建函数recover(),先来看看这个内建函数的注释。

1
2
3
4
5
6
7
8
9
10
// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}

这个函数需要用在一个defer函数内,他可以捕获一个 go 例程所抛出的panic并对其进行处理,然后恢复程序的正常运行。这个中间件明显就是使用这个来实现错误捕获的。

defer函数相当于一个析构函数,会在当前函数所有语句执行完毕之后调用,平时我们可以用来管理一些文件流、网络流的close,这里就可以用来错误捕获,来实现相当于其他语言中的 throw 的功能。

最后,gin.Default()就会返回一个默认的,带有日志和错误捕获中间件的gin实例。

这里顺便提一下,在生产环境中,日志读写的 I/O 对于并发的性能有着很大的影响,因此,在一些高并发的场景下,关闭或减少非必要日志的输出可能会带来比较高的性能提升。

1
2
3
4
5
6
7
8
// Logger
ab -n10000 -c10 127.0.0.1:3000/test
...
4.537s
// Without Logger
ab -n10000 -c10 127.0.0.1:3000/test
...
3.455s

使用 ab 测试了一下,可以看出性能有着 30%左右的影响。

Handler

有了一个gin实例之后,我们就可以为他设置路由和对应的Handler

我们可以通过gin来轻易实现 RESTful 风格的 API 接口,上面提到的IRoutes接口就实现了 RESTful 风格的各种 API 的接口。官方给出来的r.GET就是其中最常用的一个。

1
2
3
4
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers)
}

从源码中可以看到,GET,POST, PUT等各种方法的路由都是交给handle来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}

gin在这里使用的是绝对的路由路径,这样对于多层嵌套的路由性能也不会有太大的损耗。

从之前的中间件的分析中可以知道,一个路由所绑定的中间件都会被append到当前路由的group.Handlers里面的。

通过combineHandlers,将当前路由需要绑定的Handler和之前的中间件合在一起,每当这个Handler被调用的时候,就必然会先经过前面所设置的两个中间件LoggerRecovery,那样就实现了中间件的绑定。

最后一步,就是加入路由当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers) // 添加路由到基树中
}

// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handlers HandlersChain)

为了更好的性能,可以看到gin是使用了基树这种数据结构来存储路由的,基数也叫做压缩前缀树,是一种节省空间的前缀树,如果一个节点只有一个子树,那么这个子树就会和父节点合并,带来更小的空间和更优的性能。

维基百科例子:

img

根据不同的请求类型,分为不同的树,如GET TreePOST Tree等等,然后加入到enginetrees数组当中。

然后根据路由的path,生成一棵路由树,其节点存储的就是当前路由的调用链。

这样的设计无疑可以带来更好的路由性能。

在之前的一个简单的小例子里面,使用了默认的gin.Default,这里运行一下,可以看到下面的日志输出。

1
2
3
4
5
6
7
8
9
10
11
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET /list --> github.com/ZhenlyChen/cloudgo/controller.Get (3 handlers)
[GIN-debug] POST /add --> github.com/ZhenlyChen/cloudgo/controller.Add (3 handlers)
[GIN-debug] GET /test --> github.com/ZhenlyChen/cloudgo/controller.Test (3 handlers)
[GIN-debug] GET /date --> github.com/ZhenlyChen/cloudgo/controller.Date (3 handlers)
[GIN-debug] Listening and serving HTTP on :3000

可以看到,每个路由上都被绑定了三个handlers,依次就是LoggerRecovery和我们自己绑定的Handler

Run

Run就是开始工作,开始监听端口。

1
2
3
4
5
6
7
8
9
10
11
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
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
}

首先,这个函数同样是使用了defer的技巧来输出错误。

然后解析地址,最后调用内置http库的ListenAndServe方法将整个engine作为一个Handler绑定在这个端口。

接下来看看http中的实现

1
2
3
4
5
6
7
8
9
10
11
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

为什么engine可以作为Handler参数传入这个函数呢?原因就在于Handler只是一个接口,只要实现了ServeHTTP(ResponseWriter, *Request)这个方法的所有struct,都可以作为参数传入。这样,就实现了CallerCallee之间的解耦,这也是 OO 的核心思想。

调用者不需要关心被调用者的逻辑,只需要实现给定的接口,调用者就可以在适当的时候调用,来实现功能。这也是Javascript中比较常见的回调函数的一种实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and either closes the network connection or sends an HTTP/2
// RST_STREAM, depending on the HTTP protocol. To abort a handler so
// the client sees an interrupted response but the server doesn't log
// an error, panic with the value ErrAbortHandler.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

当有请求的时候,程序就会调用ServeHTTP这个函数,将请求信息和回应的ResponseWriter交给Handler处理(这里是ginengine),因此,框架可以完全不管网络层、传输层的实现,直接在应用层之上编程。把复杂的网络通信高度抽象化成一个请求、回应的接口:

gin

这里的ResponseWriter同样是一个接口,go 的网络库中实现了这个接口,我们可以通过Write([]byte) (int, error)或者
WriteHeader(statusCode int)将响应的内容返回给网络库。

1
2
3
4
5
6
7
8
9
10
11
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()

engine.handleHTTPRequest(c)

engine.pool.Put(c)
}

显然,这个接口在Engine内部已经实现好了

众所周知, go 语言是一种具有自动 GC(垃圾回收)特性的语言。这里使用了sync.Pool,是谷歌提供的一个临时对象池,是一组临时对象的组合,存储那些被分配了但是没有被使用,而未来可能会使用的值,以减小垃圾回收的压力。这是一个协程安全的操作,照高并发的情况下,使用这个可以大幅度提高性能。

当收到一个请求的时候,从临时对象池中获取一个上下文对象Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter

Params Params
handlers HandlersChain
index int8

engine *Engine

// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}

// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs

// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
}

Context是一般的 Web 框架中最常见的组件,是穿插在整个请求过程当中的一个对象。通过Context,我们可以在各个中间件之间传递变量,保存着当前上下文有关的数据和变量。

这种设计模式在并发的时候就十分有用。

首先,他限制了这个请求的数据的作用域,这个请求的数据只存在于这次请求当中,并且保证了数据的生命周期,使得数据在整个请求当中都有效,避免了使用全局变量。

然后,Context的存在使得数据的粒度更细,避免了使用冲突和过度的耦合。

从临时对象池中取出Context之后,初始化并且将当前的请求和响应的Writer绑定在Context

使用engine.handleHTTPRequest(c)将上下文交给下一步处理

最后将当前上下文重新放入临时对象池,等待下一次的使用。

handleHTTPRequest中,对请求进行判断,然后交给相应的Handler处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
path := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
path = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}

// 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
handlers, params, tsr := root.getValue(path, c.Params, unescape)
if handlers != nil {
c.handlers = handlers
c.Params = params
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}

if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}

这里就可以从之前创建的基树中查找路由,找出当前路由对应的Handler,然后将Handler绑定在当前的上下文中,调用c.Next(),将当前的上下文传递给Handler中第一个函数(在这里默认是日志中间件)。

当所有的Handler执行完毕的时候,最后就会调用c.writermem.WriteHeaderNow(),将状态通过ResponseWriter写入到响应当中,然后网络库就会将响应发送给用户,这样就完成了一个请求的响应了。

到此,官方的 DEMO 就分析完了。但是gin当中不仅仅只有这些内容,这里只是分析了一个服务端创建、监听和处理一个简单的请求的过程。

土豪通道
0%