Gin
是使用Go语言编写的高性能的web
服务框架,根据官方的测试,性能是httprouter
的40倍左右。要使用好这套框架呢,首先我们就得对这个框架的基本结构有所了解,所以我将从以下几个方面来对Gin
的源码进行解读。
Gin
是如何储存和映射URL
路径到相应的处理函数的Gin
中间件的设计思想及其实现Gin
是如何解析客户端发送请求中的参数的Gin
是如何将各类格式(JSON/XML/YAML
等)数据解析返回的Gin Github官方地址
事实上,Gin
也是基于http
包封装来实现的网络通信,底层仍旧使用的是http.ListenAndServe
来创建的监听端口和服务,只不过将接收到的数据解析为Gin
的Context
上下文后,最终再传递到type HandlerFunc func(*Context)
处理函数中去的。
再了解一个大致的数据处理过程之后,我们就从Gin
的监听入口开始逐渐摸索。
if err := router.Run();err != nil {
log.Println("something error");
}
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
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
通过上面这个过程可以了解到Gin
和http
通信框架建立联系是通过engine *Engine
实现的,同时ListenAndServe
要求传入的是一个Handler
类型的对象,而该对象定义如下:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
这咋一看,瞬间就明白了许多,ResponseWriter, *Request
这两个参数一目了然——请求与响应流
,http
包就是底层处理过后将这两个数据通过该接口传递到Gin
框架内部的,所以我们找到该接口的实现。
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)
}
在正式开始了解这个处理过程之前,我们先来了解一下Context
这个贯穿整个Gin
框架的上下文对象,在C/S通信过程中所有的数据都保存在这个对象中了。
type Context struct {
//响应输出流(私有,供框架内部数据写出)
writermem responseWriter
//客户端发送的所有信息都保存在这个对象里面
Request *http.Request
//响应输出流(公有,供给处理函数写出)
// 在初始化后,由writermem克隆而来的
Writer ResponseWriter
//保存解析得到的参数,路径中的REST参数
Params Params
//该请求对应的处理函数链,从树节点中获取
handlers HandlersChain
//记录已经被处理的函数个数
index int8
//当前请求的完整路径
fullPath string
//Gin的核心引擎
engine *Engine
//并发读写锁
KeysMutex *sync.RWMutex
//用于保存当前会话的键值对,用于不同处理函数中传递
Keys map[string]interface{}
//处理函数链输出的错误信息
Errors errorMsgs
//客户端希望接受的数据类型,如:json、xml、html
Accepted []string
//存储URL中的查询参数,如:/test?name=jhon&age=11
// 这样的参数储存在这个对象里
queryCache url.Values
//这个用于存储POST/PATCH等提交的body中的参数
formCache url.Values
//用来限制第三方 Cookie,一个int值,有Strict、Lax、None
// Strict:只有当前网页的 URL 与请求目标一致,才会带上 Cookie
// Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,
// 但是导航到目标网址的 Get 请求除外
// 设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击
sameSite http.SameSite
}
在了解完Context
后,我们来进入正式的数据解析过程:
func (engine *Engine) handleHTTPRequest(c *Context) {
//获取客户端的http请求方法
httpMethod := c.Request.Method
//获取请求的URL地址,这里的URL是进过处理的
rPath := c.Request.URL.Path
//是否不启动字符转义
unescape := false
//判断是否启用原URL,未转义字符
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
//判断是否需要移除多余的分隔符"/"
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
//首先获取到指定HTTP方法的搜索树的根节点
root := t[i].root
//从根节点开始搜索匹配该路径的节点
value := root.getValue(rPath, c.Params, unescape)
//将节点中的存储的信息,拷贝到Context上下文中
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
//这里就是在遍历执行处理函数链
// func (c *Context) Next() {
// c.index++
// for c.index < int8(len(c.handlers)) {
// c.handlers[c.index](c)
// c.index++
// }
// }
c.Next()
//写出响应状态码
c.writermem.WriteHeaderNow()
return
}
//如果没有找到对应的匹配节点,则考虑是否是以下的特殊情况
if httpMethod != "CONNECT" && rPath != "/" {
//如果启动自动重定向,删除最后的"/"并重定向
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
//启动路径修复后,当/../foo找不到匹配路由时,
// 会自动删除..部分路由,然后重新匹配直到找到匹配路由,并重定向
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
//是HTTP方法不匹配,而路径匹配则返回405
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
//如果都找不到路由则返回404
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
上述代码就是整个请求的处理过程,而节点查找和参数解析都在getValue
函数之中,我们来看一下他是如何匹配路径和参数解析的:
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
//先保存原有的REST参数列表
value.params = po
walk: //这个标号使用中递归的,这里使用的是循环式的递归
for {
// 当前节点的路径
prefix := n.path
//如果该路径与当前节点路径刚好匹配
if path == prefix {
//如果处理函数是一样的
// 则说明已经搜索过了更新路径后跳出。
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
//这种情况直接推荐重定向
if path == "/" && n.wildChild && n.nType != root {
//这个表示重定向后可以找到满足条件的节点
value.tsr = true
return
}
//如果以上条件都未匹配,则根据索引去搜索子节点
indices := n.indices
for i, max := 0, len(indices); i < max; i++ {
if indices[i] == '/' {
n = n.children[i]
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
(n.nType == catchAll && n.children[0].handlers != nil)
return
}
}
return
}
//这里这种情况说明的是path的前缀刚好和该节点吻合
//所以进入子节点搜索
if len(path) > len(prefix) && path[:len(prefix)] == prefix {
path = path[len(prefix):]
//如果该节点没有通配符子节点,则根据索引查找子节点
if !n.wildChild {
c := path[0]
indices := n.indices
for i, max := 0, len(indices); i < max; i++ {
if c == indices[i] {
n = n.children[i]
continue walk
}
}
//如果没找到匹配的子节点,则建议重定向搜索
value.tsr = path == "/" && n.handlers != nil
return
}
//下面是子节点是统配符节点的情况
// 需要根据传入的URL对路径中的参数进行解析
// 因为如果n.wildChild为true的话,那么n就只能有一个子节点
n = n.children[0]
switch n.nType {
//子节点为参数节点
case param:
//寻找参数的字符长度
end := 0
for end < len(path) && path[end] != '/' {
end++
}
//根据maxParams来预分配更大的参数列表(仅仅是容量)
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(value.params)
//拓展参数列表长度
value.params = value.params[:i+1]
//获取参数名从1开始是因为一般都是*:开头的
value.params[i].Key = n.path[1:]
// 获取参数值
val := path[:end]
//如果需要转义则调用转义函数
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
value.params[i].Value = val // fallback, in case of error
}
} else {
value.params[i].Value = val
}
//如果path还没解析完
if end < len(path) {
// 进入其子节点
if len(n.children) > 0 {
path = path[end:]
n = n.children[0]
continue walk
}
// 若仅仅是多了个"/",则推荐重定向
value.tsr = len(path) == end+1
return
}
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
if len(n.children) == 1 {
//如果子节点有匹配"/"的,则推荐重定向
n = n.children[0]
value.tsr = n.path == "/" && n.handlers != nil
}
return
//这个类型表明所有的参数都已经匹配完了
case catchAll:
//下面的过程和上面差不多
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[2:]
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
value.params[i].Value = path // fallback, in case of error
}
} else {
value.params[i].Value = path
}
//获取节点中保存的处理函数链
value.handlers = n.handlers
//获取该节点下的完整路径
value.fullPath = n.fullPath
return
default:
panic("invalid node type")
}
}
// 说明该节点是个,则只有推荐重定向了
value.tsr = (path == "/") ||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
path == prefix[:len(prefix)-1] && n.handlers != nil)
return
}
}
其实从上面都能看出,这个过程就是从搜索树的根节点依次向下搜索,每次搜索完毕后,都会更新当前路径path
,例如:Path
:/test/add
、当前节点路径为/test
,那么进入子节点后Path
就会变为/add
,按这种模式一直匹配,直到path为空或者为/
,如果是/
通常都是将value.tsr
设置为true
然后返回,这样就会使得服务器返回一个对路径优化过(/test/
优化为/test
)的重定向命令,然后再重新路由。
一般来说,客户端发送的数据一般有REST参数
、Query参数
、Form参数
、文件数据
,所以我来看看这四种数据的获取来源:
首先是一个简单的示例:
func main() {
router := gin.Default()
//curl --location --request POST \
// '127.0.0.1:8080/welcome?name=jhonson'
router.GET("/welcome", func(c *gin.Context) {
//
name := c.Query("name")
c.String(http.StatusOK, "Hello %s", name)
})
// curl --location --request POST '127.0.0.1:8080/user/jack/get'
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
// curl --location --request POST '127.0.0.1:8080/table' \
// --form 'message=everthing is ok'
router.POST("/table", func(c *gin.Context) {
message := c.PostForm("message")
c.String(http.StatusOK, message)
})
// curl -X POST http://localhost:8080/upload \
// -F "file=@/Users/appleboy/test.zip" \
// -H "Content-Type: multipart/form-data"
router.POST("/upload", func(c *gin.Context) {
//获取文件
file, _ := c.FormFile("file")
log.Println(file.Filename)
c.SaveUploadedFile(file, dst)
})
router.Run(":8080")
}
首先我们回顾一下Context
中的几个重要变量和获取参数的几个方法:
type Context struct {
...省略
//保存解析得到的参数,路径中的REST参数
Params Params
//存储URL中的查询参数,如:/test?name=jhon&age=11
// 这样的参数储存在这个对象里
queryCache url.Values
//这个用于存储POST/PATCH等提交的body中的参数
formCache url.Values
}
Query()
方法func (c *Context) Query(key string) string {
value, _ := c.GetQuery(key)
return value
}
func (c *Context) GetQuery(key string) (string, bool) {
if values, ok := c.GetQueryArray(key); ok {
return values[0], ok
}
return "", false
}
func (c *Context) GetQueryArray(key string) ([]string, bool) {
c.getQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true
}
return []string{}, false
}
func (c *Context) getQueryCache() {
if c.queryCache == nil {
c.queryCache = c.Request.URL.Query()
}
}
从这里一眼就能看出Query
参数的值来自于Context
的queryCache
Param()
方法func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
func (ps Params) ByName(name string) (va string) {
va, _ = ps.Get(name)
return
}
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
}
REST
参数来源于Context
的Params
PostForm()
方法form
参数就不再赘述,基本和Query
参数查询的过程一样,来源于Context
的formCache
FormFile()
方法FormFile()
则是获取客户端上传的文件,这有很大的不同,我们来看看:type FileHeader struct {
//文件名
Filename string
//文件型
Header textproto.MIMEHeader
//文件大小
Size int64
//文件内容(保存在内存中时)
content []byte
//临时文件名,当设置的maxMemory小于上传文件时,
// 会被磁盘化,并利用变量记录临时文件的位置
tmpfile string
}
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
if c.Request.MultipartForm == nil {
//这个就是解析form参数
if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
return nil, err
}
}
f, fh, err := c.Request.FormFile(name)
if err != nil {
return nil, err
}
f.Close()
return fh, err
}
func (r *Request) ParseMultipartForm(maxMemory int64) error {
if r.MultipartForm == multipartByReader {
return errors.New("http: multipart handled by MultipartReader")
}
if r.Form == nil {
err := r.ParseForm()
if err != nil {
return err
}
}
if r.MultipartForm != nil {
return nil
}
mr, err := r.multipartReader(false)
if err != nil {
return err
}
//我们重点看这个方法
f, err := mr.ReadForm(maxMemory)
if err != nil {
return err
}
if r.PostForm == nil {
r.PostForm = make(url.Values)
}
for k, v := range f.Value {
r.Form[k] = append(r.Form[k], v...)
// r.PostForm should also be populated. See Issue 9305.
r.PostForm[k] = append(r.PostForm[k], v...)
}
r.MultipartForm = f
return nil
}
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
form := &Form{make(map[string][]string), make(map[string][]*FileHeader)}
defer func() {
if err != nil {
form.RemoveAll()
}
}()
// 需要额外的10 MB的空间存储非Part-form的数据
maxValueBytes := maxMemory + int64(10<<20)
for {
p, err := r.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
name := p.FormName()
if name == "" {
continue
}
filename := p.FileName()
var b bytes.Buffer
//如果文件名为空,则认为客户端上传的是
//会被认为是form表单参数,添加到PostForm中,
// 最终传递到Context的formCache中
if filename == "" {
n, err := io.CopyN(&b, p, maxValueBytes+1)
if err != nil && err != io.EOF {
return nil, err
}
maxValueBytes -= n
if maxValueBytes < 0 {
return nil, ErrMessageTooLarge
}
form.Value[name] = append(form.Value[name], b.String())
continue
}
fh := &FileHeader{
Filename: filename,
Header: p.Header,
}
//读取数据到缓冲区中
n, err := io.CopyN(&b, p, maxMemory+1)
if err != nil && err != io.EOF {
return nil, err
}
//如果文件过大,则写到磁盘上的临时文件再继续读
if n > maxMemory {
// too big, write to disk and flush buffer
file, err := ioutil.TempFile("", "multipart-")
if err != nil {
return nil, err
}
size, err := io.Copy(file, io.MultiReader(&b, p))
if cerr := file.Close(); err == nil {
err = cerr
}
if err != nil {
os.Remove(file.Name())
return nil, err
}
//内存容量不足时,将tmpfile记录为临时文件名称
fh.tmpfile = file.Name()
fh.Size = size
} else {
//如果文件能存储在内存中,就记录数据位置
fh.content = b.Bytes()
fh.Size = int64(len(fh.content))
maxMemory -= n
maxValueBytes -= n
}
form.File[name] = append(form.File[name], fh)
}
return form, nil
}
这个文件获取的过程比较长,我就只对比较关键的位置进行了注释。概括一下就是客户端传过来的文件,最初会被写入到缓冲区(大小由maxMemory
决定)中。如果缓冲区无法容纳整个文件时,就会被写入到临时文件夹中,作为一个临时文件被磁盘化,而FileHeader.tmpfile
就记录了临时文件的位置。如果缓冲区能够容纳时,则返回缓冲区中有效数据的字节数组切片,保存在FileHeader.content
中。所以拿到FileHeader
就相当于拿到客户端传过来的文件数据了。