0%

Gin Web框架-上篇.md

一、net-http

1 介绍

1
2
3
4
5
6
7
.
├── ClientGet
│   └── main.go  // 发送get请求
├── ClientPost
│   └── main.go  // 发送post请求
├── Server
│   └── main.go  // web服务
  • Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现。

2 web服务

1、Server/main.go

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
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)

type Data struct {
Name string `json:"name"`
}

// 处理POST请求: http://127.0.0.1:8005/req/post {"name": "root"}
func dealPostRequestHandler(w http.ResponseWriter, r *http.Request) {
// 请求体数据
bodyContent, _ := ioutil.ReadAll(r.Body)
strData := string(bodyContent)
var d Data
json.Unmarshal([]byte(strData), &d)
fmt.Printf("body content:[%s]\n", string(bodyContent))
//返回响应内容
json.NewEncoder(w).Encode(fmt.Sprintf("收到名字:%s", d.Name))
}

// 处理GET请求: http://127.0.0.1:8005/req/get?name=root
func dealGetRequestHandler(w http.ResponseWriter, r *http.Request) {
// 获取请求的参数
query := r.URL.Query()
jsonStr, _ := json.Marshal(query)
fmt.Println("get request param: " + string(jsonStr) + "\n")
// 解析请求参数
name, _ := query["name"]
json.NewEncoder(w).Encode(name)
}

func main() {
http.HandleFunc("/req/post", dealPostRequestHandler)
http.HandleFunc("/req/get", dealGetRequestHandler)
http.ListenAndServe(":8005", nil)
}

2、postman测试

{width=”6.02363188976378in” height=”6.1725853018372705in”}

3 请求数据

1、ClientGet/main.go

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
package main

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
)

func main() {
requestGet()
}

func requestGet() {
apiUrl := "http://127.0.0.1:8005/req/get"
data := url.Values{}
data.Set("name", "root")
u, _ := url.ParseRequestURI(apiUrl)
u.RawQuery = data.Encode() // URL encode
fmt.Println("请求路由为:", u.String())
resp, _ := http.Get(u.String())
b, _ := ioutil.ReadAll(resp.Body)
fmt.Println("返回数据为:", string(b))
}

/*
请求路由为: http://127.0.0.1:8005/req/get?name=root
返回数据为: ["root"]
*/

2、ClientPost/main.go

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
package main

import (
"fmt"
"io/ioutil"
"net/http"
"strings"
)

func main() {
requestPost()
}

func requestPost() {
url := "http://127.0.0.1:8005/req/post"
// 表单数据 contentType := "application/x-www-form-urlencoded"
contentType := "application/json"
data := `{"name":"rootPort"}`
resp, _ := http.Post(url, contentType, strings.NewReader(data))
b, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(b))
}

/*
"收到名字:rootPort"
*/

二、Gin Web框架基本使用-原理篇

2.1 Gin入门

1、介绍

  • Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点

  • 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错

  • 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范

2、安装

要安装Gin软件包,您需要安装Go并首先设置Go工作区。

1.首先需要安装Go(需要1.10+版本),然后可以使用下面的Go命令安装Gin。

1
go get -u github.com/gin-gonic/gin

2.将其导入您的代码中:

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

3.(可选)导入net/http。例如,如果使用常量,则需要这样做http.StatusOK。

1
import "net/http"

3、hello word

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
   "net/http"
   "github.com/gin-gonic/gin"
)
func main() {
   // 1.创建路由
  r := gin.Default()
  // 2.绑定路由规则,执行的函数
  // gin.Context,封装了request和response
  r.GET("/", func(c *gin.Context) {
     c.String(http.StatusOK, "hello World!")
  })
  // 3.监听端口,默认在8080
  // Run("里面不指定端口号默认为8080")
  r.Run(":8000")
}

2.2 Gin工作流程

1、gin核心概念

  • Engine 容器对象,整个框架的基础
  • Engine.trees 负责存储路由和handle方法的映射,采用类似字典树的结构
  • Engine.RouterGroup ,其中的Handlers存储着所有中间件
  • Context 上下文对象,负责处理请求和回应 ,其中的handlers 是存储处理请求时中间件和处理方法

2、请求生命周期

3.3 Gin源码

[gin源码](# gin源码)

三、Gin Web框架基本使用-基础篇

3.1 路由与传参

1、无参路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"net/http"

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

func HelloWorldHandler(c *gin.Context) {
// gin.Context,封装了request和response
c.String(http.StatusOK, "hello World!")
}

func main() {
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// 基本路由 /hello/
r.GET("/hello", HelloWorldHandler)
// 3.监听端口,默认在8080
fmt.Println("运行地址:http://127.0.0.1:8000")
r.Run(":8000")
}
2、API参数
  • 可以通过Context的Param方法来获取API参数
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
package main

import (
"fmt"
"net/http"

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

func GetBookDetailHandler(c *gin.Context) {
bookId := c.Param("id")
// gin.Context,封装了request和response
c.String(http.StatusOK, fmt.Sprintf("成功获取书籍详情:%s", bookId))
}

func main() {
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// 基本路由 /book/24/
r.GET("/book/:id", GetBookDetailHandler)
// 3.监听端口,默认在8080
fmt.Println("运行地址:http://127.0.0.1:8000/book/24/")
r.Run(":8000")
}
3、url参数
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
package main

import (
"fmt"
"net/http"

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

func GetUserDetailHandler(c *gin.Context) {
//username := c.DefaultQuery("name", "xxx")
username := c.Query("name")
// gin.Context,封装了request和response
c.String(http.StatusOK, fmt.Sprintf("姓名:%s", username))
}

func main() {
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// 基本路由 /user?name=root
r.GET("/user/", GetUserDetailHandler)
// 3.监听端口,默认在8080
fmt.Println("运行地址:http://127.0.0.1:8000/user?name=root")
r.Run(":8000")
}
4、ShouldBind参数绑定
  • 我们可以基于请求的Content-Type 识别请求数据类型并利用反射机制
  • 自动提取请求中QueryString 、form表单 、JSON 、XML 等参数到结构体中
  • 下面的示例代码演示了.ShouldBind() 强大的功能
  • 它能够基于请求自动提取JSON 、form表单 和QueryString 类型的数据,并把值绑定到指定的结
    构体对象。
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
package main

import (
"fmt"
"net/http"

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

// Binding from JSON
type Login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}

func LoginHandler(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err != nil {
// 如果数据校验不通过直接返回
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
c.String(http.StatusOK, fmt.Sprintf("姓名:%s -- 密码:%s", login.Username,
login.Password))
}

func main() {
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// 基本路由 /login/
r.POST("/login/", LoginHandler)
// 3.监听端口,默认在8080
fmt.Println("运行地址:http://127.0.0.1:8000/login/")
r.Run(":8000")
}

postman测试

3.2 响应返回

1、响应String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"net/http"

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

func main() {
r := gin.Default()
r.GET("/response/", ResponseStringHandler)
fmt.Println("http://127.0.0.1:8000/response/")
r.Run(":8000")
}

func ResponseStringHandler(c *gin.Context) {
c.String(http.StatusOK, "返回简单字符串")
}

2、响应JSON

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
package main

import (
"fmt"
"net/http"

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

func main() {
r := gin.Default()
r.GET("/response/", ResponseJsonHandler)
fmt.Println("http://127.0.0.1:8000/response/")
r.Run(":8000")
}

func ResponseJsonHandler(c *gin.Context) {
type Data struct {
Msg string `json:"msg"`
Code int `json:"code"`
}
d := Data{
Msg: "Json数据",
Code: 1001,
}
c.JSON(http.StatusOK, d)

//// 也可以直接使用 gin.H返回 json数据
//c.JSON(http.StatusOK, gin.H{
// "msg": "success",
//})
}

3、路由重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"net/http"

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

func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://tigerfivegit.github.io/")
})
fmt.Println("http://127.0.0.1:8000")
r.Run(":8000")
}

3.3 路由分发

1、项目结构

1
2
3
4
5
6
7
.
├── go.mod
├── go.sum
├── main.go
└── routers
   ├── books.go
   └── users.go
2、main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
// demo_roue_layer 项目根目录
// go.mod文件中执行 module demo_route_layer
"demo_roue_layer/routers"
"fmt"

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

func main() {
//r本质 实例化*gin.Engine结构体
r := gin.Default()
routers.LoadBooks(r)
routers.LoadUsers(r)
fmt.Println("用户路由: http://127.0.0.1:8000/user")
fmt.Println("书籍路由: http://127.0.0.1:8000/book")
r.Run(":8000")
}
3、routers/users.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package routers

import (
"net/http"

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

func LoadUsers(e *gin.Engine) {
e.GET("/user", UserHandler)
}

func UserHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "User Router",
})
}
4、routers/books.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package routers

import (
"net/http"

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

func LoadBooks(e *gin.Engine) {
e.GET("/book", GetBookHandler)
}

func GetBookHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Book Router",
})
}

测试路由分发效果

{width=”6.383172572178478in” height=”2.0739752843394577in”}

END

gin源码

00.前置知识

0.1 基数树(Radix Tree)

  • Radix Tree可以被认为是一棵简洁版的前缀树。
  • 我们注册路由的过程就是构造前缀树的过程,具有公共前缀的节点也共享一个公共父节点
  • 由于URL路径具有层次结构,并且只使用有限的一组字符(字节值),所以很可能有许多常见的前缀。
  • 这使我们可以很容易地将路由简化为更小的问题。
  • gin中,路由器为每种请求方法管理一棵单独的树

img

0.2 net/http

  • Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现
  • ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。
  • 处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}

func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9090", nil)
if err != nil {
fmt.Printf("http server failed, err:%v\n", err)
return
}
}

img

0.3 运行gin服务

  • 创建一个测试项目

img

  • gin服务代码
1
C:\aaa\gin_demo>  go mod tidy	  // 更新依赖包
1
2
3
4
5
6
7
8
9
10
11
12
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 (for windows "localhost:8080")
}
  • 运行效果

img

0.4 gin核心概念

  • Engine 容器对象,整个框架的基础
  • Engine.trees 负责存储路由和handle方法的映射,采用类似字典树的结构
  • Engine.RouterGroup,其中的Handlers存储着所有中间件
  • Context上下文对象,负责处理请求和回应,其中的handlers是存储处理请求时中间件和处理方法的

01.第一步: 初始化容器

  • 1)初始化容器流程

    • 通过调用 gin.New() 方法来实例化Engine容器.
    • 虽然参数很多,但我们只需要注意 RouterGroup ,treesengine.pool.New即可
    • engine.pool.New 负责创建Context对象,采用sync.Pool减少频繁context实例化带来的资源消耗
  • 2)Engine说明

    • 在整个gin框架中最重要的一个struct就是Engine, 它包含路由, 中间件, 相关配置信息等.

    • Engine的代码主要就在gin.go中

    • Engine中比较重要的几个属性, 其他的属性暂时全部省略掉

    • ```go
      type Engine struct {

         RouterGroup // 路由
         pool             sync.Pool  // context pool
         trees            methodTrees // 路由树
         // html template及其他相关属性先暂时忽略
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      - Engine有几个比较主要的函数 `New(), Default()`

      ### 1.1 先看 `gin.Default()`

      - Default()跟New()几乎一模一样, 就是调用了gin内置的Logger(), Recovery()中间件.

      ```go
      func Default() *Engine {
      debugPrintWARNINGDefault()
      engine := New()
      engine.Use(Logger(), Recovery())
      return engine
      }

1.2 engine := New() 初始化

  • 通过调用 gin.New() 方法来实例化Engine容器.
  • 1.初始化了Engine
  • 2.将RouterGroup的Handlers(数组)设置成nil, basePath设置成 /
  • 3.为了使用方便, RouteGroup里面也有一个Engine指针, 这里将刚刚初始化的engine赋值给了RouterGroup的engine指针
  • 4.为了防止频繁的context GC造成效率的降低, 在Engine里使用了sync.Pool, 专门存储gin的Context
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
func New() *Engine {
debugPrintWARNINGNew()
// Engine 容器对象,整个框架的基础
engine := &Engine{ // 初始化语句
// Handlers 全局中间件组在注册路由时使用
RouterGroup: RouterGroup{ // Engine.RouterGroup,其中的Handlers存储着所有中间件
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedProxies: []string{"0.0.0.0/0"},
AppEngine: defaultAppEngine,
UseRawPath: false,
RemoveExtraSlash: 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
}

02.第二步: 注册中间件

2.1 engine.Use()

  • gin框架中的中间件设计很巧妙,我们可以首先从我们最常用的r := gin.Default()Default函数开始看
  • 它内部构造一个新的engine之后就通过Use()函数注册了Logger中间件和Recovery中间件
  • Use()就是gin的引入中间件的入口了.
  • 仔细分析这个函数, 不难发现Use()其实是在给RouteGroup引入中间件的.
  • 具体是如何让中间件在RouteGroup上起到作用的, 等说到RouteGroup再具体说.
1
2
3
4
5
6
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}

2.2 往下查看Use()函数代码

gin.use() 调用RouterGroup.Use()RouterGroup.Handlers写入记录

1
2
3
4
5
6
7
8
9
10
11
12
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers() //注册404处理方法
engine.rebuild405Handlers() //注册405处理方法
return engine
}

// 其中`Handlers`字段就是一个数组,用来存储中间件
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
  • 而我们注册路由时会将对应路由的函数和之前的中间件函数结合到一起
1
2
3
4
5
6
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()
}

2.3 组成一条处理函数链条HandlersChain

  • 其中结合操作的函数内容如下,注意观察这里是如何实现拼接两个切片得到一个新切片的
1
2
3
4
5
6
7
8
9
10
11
12
const abortIndex int8 = math.MaxInt8 / 2

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
}
  • 也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain
  • 而它本质上就是一个由HandlerFunc组成的切片
1
type HandlersChain []HandlerFunc

2.4 中间件的执行

  • 其中

    1
    c.Next()

    就是很关键的一步,它的代码很简单

    • 从下面的代码可以看到,这里通过索引遍历HandlersChain链条
    • 从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)
1
2
3
4
5
6
7
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
  • 我们可以在中间件函数中通过再次调用c.Next()实现嵌套调用(func1中调用func2;func2中调用func3)
  • 或者通过调用c.Abort()中断整个调用链条,从当前函数返回
1
2
3
func (c *Context) Abort() {
c.index = abortIndex // 直接将索引置为最大限制值,从而退出循环
}
  • c.Set()/c.Get()
    • c.Set()c.Get()这两个方法多用于在多个函数之间通过c传递数据的
    • 比如我们可以在认证中间件中获取当前请求的相关信息(userID等)通过c.Set()存入c
    • 然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户
    • c就像是一根绳子,将该次请求相关的所有的函数都串起来了。

03.第三步: 注册路由

  • 注册路由的逻辑主要有addRoute函数和insertChild方法

3.1 注册路由动图

  • 1、第一次注册路由,例如注册search
  • 2、继续注册一条没有公共前缀的路由,例如blog
  • 3、注册一条与先前注册的路由有公共前缀的路由,例如support

img

  • 路由树节点
    • 路由树是由一个个节点构成的,gin框架路由树的节点由node结构体表示
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
// tree.go

type node struct {
// 节点路径,比如上面的s,earch,和upport
path string
// 和children字段对应, 保存的是分裂的分支的第一个字符
// 例如search和support, 那么s节点的indices对应的"eu"
// 代表有两个分支, 分支的首字母分别是e和u
indices string
// 儿子节点
children []*node
// 处理函数链条(切片)
handlers HandlersChain
// 优先级,子节点、子子节点等注册的handler数量
priority uint32
// 节点类型,包括static, root, param, catchAll
// static: 静态节点(默认),比如上面的s,earch等节点
// root: 树的根节点
// catchAll: 有*匹配的节点
// param: 参数节点
nType nodeType
// 路径上最大参数个数
maxParams uint8
// 节点是否是参数节点,比如上面的:post
wildChild bool
// 完整路径
fullPath string
}

3.2 addRoute构造路由树

  • 这段代码就是利用method, path, 将handlers注册到engine的trees中.
  • 注意这里为什么是HandlersChain呢, 可以简单说一下, 就是将中间件和处理函数都注册到method, path的tree中了.
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// tree.go

// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
numParams := countParams(path) // 数一下参数个数

// 空树就直接插入当前节点
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0

walk:
for {
// 更新当前节点的最大参数个数
if numParams > n.maxParams {
n.maxParams = numParams
}

// 找到最长的通用前缀
// 这也意味着公共前缀不包含“:”"或“*” /
// 因为现有键不能包含这些字符。
i := longestCommonPrefix(path, n.path)

// 分裂边缘(此处分裂的是当前树节点)
// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分
// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点
if i < len(n.path) {
child := node{
path: n.path[i:], // 公共前缀后的部分作为子节点
wildChild: n.wildChild,
indices: n.indices,
children: n.children,
handlers: n.handlers,
priority: n.priority - 1, //子节点优先级-1
fullPath: n.fullPath,
}

// Update maxParams (max of all children)
for _, v := range child.children {
if v.maxParams > child.maxParams {
child.maxParams = v.maxParams
}
}

n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}

// 将新来的节点插入新的parent节点作为子节点
if i < len(path) {
path = path[i:]

if n.wildChild { // 如果是参数节点
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++

// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--

// 检查通配符是否匹配
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
// 检查更长的通配符, 例如 :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue walk
}
}

pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
// 取path首字母,用来与indices做比较
c := path[0]

// 处理参数后加斜线情况
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}

// 检查路path下一个字节的子节点是否存在
// 比如s的子节点现在是earch和upport,indices为eu
// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点
for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}

// 否则就插入
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
// 注意这里是直接拼接第一个字符到n.indices
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
fullPath: fullPath,
}
// 追加子节点
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handlers)
return
}

// 已经注册过的节点
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
return
}
}

3.3 insertChild

  • insertChild函数是根据path本身进行分割,将/分开的部分分别作为节点保存,形成一棵树结构。
  • 参数匹配中的:*的区别是,前者是匹配一个字段而后者是匹配后面所有的路径。
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
// 找到所有的参数
for numParams > 0 {
// 查找前缀直到第一个通配符
wildcard, i, valid := findWildcard(path)
if i < 0 { // 没有发现通配符
break
}

// 通配符的名称必须包含':' 和 '*'
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
}

// 检查通配符是否有名称
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}

// 检查这个节点是否有已经存在的子节点
// 如果我们在这里插入通配符,这些子节点将无法访问
if len(n.children) > 0 {
panic("wildcard segment '" + wildcard +
"' conflicts with existing children in path '" + fullPath + "'")
}

if wildcard[0] == ':' { // param
if i > 0 {
// 在当前通配符之前插入前缀
n.path = path[:i]
path = path[i:]
}

n.wildChild = true
child := &node{
nType: param,
path: wildcard,
maxParams: numParams,
fullPath: fullPath,
}
n.children = []*node{child}
n = child
n.priority++
numParams--

// 如果路径没有以通配符结束
// 那么将有另一个以'/'开始的非通配符子路径。
if len(wildcard) < len(path) {
path = path[len(wildcard):]

child := &node{
maxParams: numParams,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n = child // 继续下一轮循环
continue
}

// 否则我们就完成了。将处理函数插入新叶子中
n.handlers = handlers
return
}

// catchAll
if i+len(wildcard) != len(path) || numParams > 1 {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}

if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
}

// currently fixed width 1 for '/'
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}

n.path = path[:i]

// 第一个节点:路径为空的catchAll节点
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
fullPath: fullPath,
}
// 更新父节点的maxParams
if n.maxParams < 1 {
n.maxParams = 1
}
n.children = []*node{child}
n.indices = string('/')
n = child
n.priority++

// 第二个节点:保存变量的节点
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handlers: handlers,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}

return
}

// 如果没有找到通配符,只需插入路径和句柄
n.path = path
n.handlers = handlers
n.fullPath = fullPath
}

04.第四步: 启动

  • 通过调用net/http来启动服务,由于engine实现了ServeHTTP方法
  • 只需要直接传engine对象就可以完成初始化并启动
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
r.Run()
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) // gin使用net/http模块
return
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
//来自 net/http 定义的接口,只要实现了这个接口就可以作为处理请求的函数
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
//实现了ServeHTTP方法
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)
}

05.第五步: 处理请求

  • 这里只需要留意handleHTTPRequest(c *Context)方法就好了
  • 通过请求方法和路由找到相对应的树节点,获取储存的[]HandlerFunc列表,通过调用c.Next()处理请求
  • 通过不停的移动下标递归,最后完成处理返回结果
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
func (engine *Engine) handleHTTPRequest(c *Context) {
...
// engine.trees:请求方法树,负责存储路由和handle方法的映射,采用类似字典树的结构
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
...
// Find route in tree
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
...
}
...
}

//这里挺巧妙的,通过不停的移动下标递归,最后完成处理返回结果
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}

06.参考博客

  • Go源码阅读】(十):Go语言Web框架GIN源码阅读

https://www.bilibili.com/video/BV1ap4y197Qj?from=search&seid=5844526642048212379

https://jiajunhuang.com/articles/2018_03_16-gin_source_code.md.html

  • Go源码阅读】(十一):Golang Web框架GIN路由/httprouter源码阅读

https://www.bilibili.com/video/BV1xf4y1m7bW?from=search&seid=1034343815651857298

https://jiajunhuang.com/articles/2020_05_09-httprouter.md.html

  • 思维导图

https://www.processon.com/view/link/5f4b70c2079129356ec5cb70#map

  • 七米

https://www.liwenzhou.com/posts/Go/read_gin_sourcecode/#autoid-0-1-1