Gin Crash Course

Gin Web Framework

Gin是Go语言编写的Web框架,功能完善,使用简单,性能高,Gin核心的路由功能是通过HttpRouter实现,具有很高的路由性能.

Web服务核心功能

  • 基础功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 通信协议: HTTP/HTTPS/gRPC
    # 通信格式: JSON/Protobuf
    # 路由匹配
    > HTTP方法,请求路径匹配到处理这个请求的函数,最终由改函数处理这次请求,并返回结果
    # 路由分组
    # 一进程多服务
    # 业务处理
    1. 参数解析
    2. 参数校验
    3. 逻辑处理
    4. 返回结果
  • 高级功能

    1
    2
    3
    # 中间件
    > Gin支持中间件,HTTP请求在转发到实际的处理函数之前,会被一系列加载的中间件进行处理
    gin.Engine.Use() 方法加载中间件

    gin.Logger(): Logger中间件将日志写到gin.DefaultWriter, gin.DefaultWriter默认为os.Stdout
    gin.Recovery(): Recovery中间件可以从任何panic恢复,并写入500状态
    gin.CustomRecovery(handle gin.RecoverFunc):
    gin.BasicAuth(): HTTP 请求基本认证

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

    |中间件 | 功能 |
    |:--------|:----------------------|
    | gin-jwt | JWT中间件,实现JWT认证|
    | gin-swagger| 自动生成Swagger 2.0格式的RESTful API文档|
    | cors| 实现HTTP请求跨域|
    |sessions|会话管理中间件|
    |authz| 基于casbin授权中间件|
    |pprof| gin pprof 中间件|
    |go-gin-prometheus|Prometheus metrics exporter|
    |gzip| 支持HTTP请求和响应的gzip压缩|
    |gin-limit|HTTP请求并发控制中间件|
    |requestid|给每个Request生成uuid,并添加在返回的X-Request-ID Header中|

    # 认证

    认证: Authentication: 用来验证某个用户是否具有访问系统的权限 – 证明你是谁
    授权: Authorization: 用来验证某个用户是否具有访问某个资源的权限 – 决定你能做什么

    1
    1. Basic: 基础认证

    用户名:密码: base64编码后,放到HTTP Authorization Header
    github.com/chyidl/noone via 🐹 v1.17
    ➜ basic=echo -n 'admin:Admin@2021'|base64

    github.com/chyidl/noone via 🐹 v1.17
    ➜ echo $basic
    YWRtaW46QWRtaW5AMjAyMQ==

    github.com/chyidl/noone via 🐹 v1.17
    ➜ echo $basic | base64 –decode
    admin:Admin@2021%

    Basic认证简单,但是不安全, 使用Basic认证+SSL配合使用确保整个认证过程安全

    不要再请求参数中使用明文密码,不要在任何存储中保存明文密码

    1
    2
    3
    4
    2. Digest
    3. OAuth
    4. Bearer
    > Bearer认证称为令牌认证。是一种HTTP身份验证方式. Bearer认证的核心是bearer token.

    bearer token: 是一个加密字符串,由服务端根据密钥生成, 客户端在请求服务端时,必须在请求头包含Authorization: Bearer . 服务端收到请求头,解析出.校验合法性,Bearer认证配合HTTPS使用,保证认证安全性

    JSON Web Token: JWT
    JWT是Bearer Token一个具体实现,由JSON数据格式组成, 通过HASH散列算法生成字符串.

    JWT 认证流程:

    不要存放敏感信息到Token中
    Payload exp值不要设置太大,一般开发版本2小时,上线版本7天

    1. 客户端使用用户名和密码请求登陆
    2. 服务端收到请求后,会验证用户名和密码,如果用户名和密码验证成功,服务端签发Token返回给客户端
    3. 客户端收到请求后将Token缓存起来 Cookie中或者LocalStorage,之后每次请求都会携带该Token
    4. 服务端收到请求后,验证请求中的Token,验证通过则进行业务逻辑处理,处理完返回处理结果

    JWT格式:

    1.Header

    - 类型声明 JWT
    - 声明加密算法 HMAC SHA256
    - 密钥ID 可选
      {
        "typ": "JWT",
        "alg": "HS256"
        "kid": "youknowwhoami"
      }
      Header进行base64编码
      github.com/chyidl/noone via 🐹 v1.17
      ➜ echo -n '{"typ":"JWT","alg":"HS256","kid":"youknowwhoami"}'|base64
      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6InlvdWtub3d3aG9hbWkifQ==
    

    2.Payload

    - JWT标准中注册的声明
    
    1
    2
    3
    4
    5
    6
    7
    iss(Issuer): JWT Token的签发者
    sub(Subject): 主题
    exp(Expiration Time): JWT Token过期时间
    aud(Audience): 接受JWT Token的一方
    iat(Issued At): JWT Token签发时间
    nbf(Not Before): JWT Token生效时间
    jti(JWT ID): JWT Token ID,令牌的唯一标识符
    - 公共的声明 > 添加用户相关信息或者其他业务需要信息 - 私有的声明 > 客户端和服务端共同定义的声明,base64是对称加密,不建议存放敏感信息

    3.Signature 签名

    - header(base64后的)
    - payload(base64后)
    - secretKey: 密钥,保存在服务器中,通过配置文件保存
    - Salt(加密盐)
    
    1
    服务端收到Token后会解析出header.payload 然后用相同的加密算法和密钥对header.payload在进行一次加密,并对加密后的Token和收到的Token是否相同,如果相同则验证通过,不相同返回HTTP 401 Unauthrozied

RequestID

> 定于和跟踪RequestID

跨域

> 当前软件架构中采用前后端分离,前端访问地址和后端方法地址不同,Web服务需要处理浏览器跨域请求

优雅关停

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
```
// main.go
package main

import (
"fmt"
"log"
"net/http"
"os"
"sync"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)

type Product struct {
Username string `json:"username" binding:"required"`
Name string `json:"name" binding:"required"`
Category string `json:"category" binding:"required"`
Price int `json:"price" binding:"gte=0"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
}

type productHandler struct {
sync.RWMutex
products map[string]Product
}

func newProductHandler() *productHandler {
return &productHandler{
products: make(map[string]Product),
}
}

func (u *productHandler) Create(c *gin.Context) {
u.Lock()
defer u.Unlock()

// 1. 参数解析
var product Product
// 将Body中JSON格式数据解析到指定的Struct中
if err := c.ShouldBindJSON(&product); err != nil {
// 返回JSON格式数据
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 2. 参数校验
if _, ok := u.products[product.Name]; ok {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("product %s already exist", product.Name)})
return
}
product.CreatedAt = time.Now()

// 3. 逻辑处理
u.products[product.Name] = product
log.Printf("Register product %s success", product.Name)

// 4. 返回结果
c.JSON(http.StatusOK, product)
}

func (u *productHandler) Get(c *gin.Context) {
u.Lock()
defer u.Unlock()

product, ok := u.products[c.Param("name")]
if !ok {

c.JSON(http.StatusNotFound, gin.H{"error": fmt.Errorf("can not found product %s", c.Param("name"))})
return
}

c.JSON(http.StatusOK, product)
}

func router() http.Handler {
router := gin.Default()
productHandler := newProductHandler()
// 路由分组/中间件/认证
v1 := router.Group("/v1")
productv1 := v1.Group("/products")
productv1.POST("", productHandler.Create)
// Gin支持两种路由匹配规则
// /products/:name 精确匹配
// /products/*name 模糊匹配
productv1.GET(":name", productHandler.Get)
return router
}

func main() {
var eg errgroup.Group

// 一进程多端口
// Gin是基于net/http包封装的web框架
insecureServer := &http.Server{
Addr: ":8080",
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

secureServer := &http.Server{
Addr: ":8443",
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

eg.Go(func() error {
err := insecureServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})

eg.Go(func() error {
path, err := os.Getwd()
if err != nil {
log.Println(err)
}
log.Println(path)
err = secureServer.ListenAndServeTLS("./server.pem", "./server.key")
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})

if err := eg.Wait(); err != nil {
log.Fatal(err)
}
}

Features

  • 支持HTTP方法: GET/POST/PUT/PATCH/DELETE/OPTIONS
  • 支持不同位置的HTTP参数:
    • 路径参数 path tag uri /user/:name name就是路径参数
      1
      路径参数: ShouldBindUri, BindUri
    • 查询字符串参数 query tag form /welcome?firstname=xx firstname就是查询参数
      1
      查询字符串参数: ShouldBindQuery, BindQuery
    • 表单参数 form tag form curl -X POST -F ‘username=colins’ http://mydomain.com/login, username就是表单参数
      1
      表单参数: ShouldBind
    • HTTP头参数 header tag header curl -X POST -H ‘Content-Type:application/json’ http://mydomain.com/login Content-Type就是HTTP头参数
      1
      HTTP头参数: ShouldBindHeader, BindHeader
    • 消息体参数 body tag json/xml curl -X POST -H ‘Content-Type:application/json’ -d ‘{“username”:”colins”}’ http://mydomain.com/login, username就是消息体参数
      1
      消息体参数: ShouldBindJSON, BindJSON
  • 支持HTTP路由和路由分组
  • 支持自定义Log
  • 支持binding和validation,
  • 支持重定向
  • 支持basic auth middleware
  • 支持自定义HTTP配置
  • 支持优雅关闭
  • 支持HTTP2
  • 支持设置获取cookie

快速入门

  • ping-pong

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

    import (
    "net/http"

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

    func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"message": "pong"})
    })

    r.Run()
    }
    • 启动 & 运行
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      ➜ go run main.go
      [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 /ping --> main.main.func1 (3 handlers)
      [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
      [GIN-debug] Listening and serving HTTP on :8080

      # 调用
      ➜ curl -X GET http://localhost:8080/ping
      {"message":"pong"}%
  • 源码分析

    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
    # gin.Default

    // Default returns an Engine instance with the Logger and Recovery middleware already attached.
    func Default() *Engine {
    debugPrintWARNINGDefault() // 检查Go版本是否达到Gin最低要求
    engine := New()
    engine.Use(Logger(), Recovery()) // 引入中间件
    return engine
    }

    # gin.New
    // 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,
    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, // const defaultMultipartMemory = 32 << 20 // 32 MB
    trees: make(methodTrees, 0, 9),
    delims: render.Delims{Left: "{{", Right: "}}"}, // HTML 模版左右定界符
    secureJSONPrefix: "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
    return engine.allocateContext()
    }
    return engine
    }

    # r.GET()

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

    # r.Run()

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

    trustedCIDRs, err := engine.prepareTrustedCIDRs()
    if err != nil {
    return err
    }
    engine.trustedCIDRs = trustedCIDRs
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
    }

    // 上下文池化防止频繁生成上下文对象,提高性能
    // ServeHTTP conforms to the http.Handler interface.
    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) // sync.Pool 对象池中获取一个上下文对象
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c) // 返回对象池
    }