Hello Gin

中文文档

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

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

func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据
c.JSON(200, gin.H{
"message": "Hello world!",
})
})

// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}

先剖析一波:

1
2
3
4
5
6
7
8
r.GET("/hello", func(c *gin.Context){
// 参数1:相应地址
// 参数2:相应函数,可以单独定义,参数必须是类型,返回的内容写入c即可
c.JSON(200, gin.H{
"message": "Hello world!",
})
// 这就是返回的JSON的例子
})

Restful 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
26
27
func main() {
r := gin.Default()
r.GET("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "GET",
})
})

r.POST("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "POST",
})
})

r.PUT("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "PUT",
})
})

r.DELETE("/book", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "DELETE",
})
})
}
// 相同名字可以不同的请求。

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
func main() {
r := gin.Default()
r.GET("/someJSON2", func(c *gin.Context) {
// 方式0.9:使用map
data:=map[string] interface{}{
"name":"小王子",
}
c.JSON(http.StatusOK, data)
})
// gin.H 是map[string]interface{}的缩写
r.GET("/someJSON", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
})

r.GET("/moreJSON", func(c *gin.Context) {
// 方法二:使用结构体
var msg struct {
Name string `json:"user"`
Message string
Age int
}
// 加了tag`json:"user"` ,在json包时就会按照json里面的名字返回
// 注意:结构体响应这里,小写表明不可导出,所以【必须大写!!!】
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}

获取参数

获取querystring参数

querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:

hl:DefaultQuery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
username := c.DefaultQuery("username", "小王子") // 表示获取名字,若没有则用默认名字
//username := c.Query("username")
address := c.Query("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run()
}

获取form参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
// DefaultPostForm取不到值时会返回指定的默认值
//username := c.DefaultPostForm("username", "小王子")
username := c.PostForm("username")
address := c.PostForm("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}

获取JSON参数

1
2
3
4
5
6
7
8
9
10
11
r.POST("/json", func(c *gin.Context) {
// 注意:下面为了举例子方便,暂时忽略了错误处理
b, _ := c.GetRawData() // 从c.Request.Body读取请求数据
// 定义map或结构体
var m map[string]interface{}
// 反序列化
_ = json.Unmarshal(b, &m)

c.JSON(http.StatusOK, m)
})

获取path参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})

r.Run(":8080")
}

参数绑定(最便捷)

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryStringform表单JSONXML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSONform表单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
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
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
// 后面的tag是为了保证和用户的对应上
// 地址栏请求时form,json是发送请求的时候。
Password string `form:"password" json:"password" binding:"required"`
}

func main() {
router := gin.Default()

// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
router.POST("/loginJSON", func(c *gin.Context) {
var login Login

if err := c.ShouldBind(&login); err == nil {
// 注意这里要加引用,要把c的内容绑定到login结构体中,如果没报错,那么继续执行。如果用u使用的是副本
fmt.Printf("login info:%#v\n", login)
// 结构化打印结构体
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})

// 绑定form表单示例 (user=q1mi&password=123456)
router.POST("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})

// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
router.GET("/loginForm", func(c *gin.Context) {
var login Login
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})

// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}

ShouldBind会按照下面的顺序解析请求中的数据完成绑定:

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSONXML,然后再使用 Formform-data)。

文件上传

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
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单个文件
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}

log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}

当多文件上传时,用for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]

for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}

路由

重定向

http重定向(浏览器地址变化)

1
2
3
4
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})

路由重定向(浏览器地址不变化)

1
2
3
4
5
6
7
8
9
10
r.GET("/test", func(c *gin.Context) {
// 指定重定向的URL
c.Request.URL.Path = "/test2"
r.HandleContext(c)
// 重定向到"/test2,但是浏览器地址不会变化。
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world"})
})

接收任意方法 Any 与错误的路由

1
2
3
4
5
6
7
8
r.Any("/user",func(c *gin.Context){
switch c.Request.Method
case "GET"
C.JSON(http.StatusoK,gin.H{"method":"GET"})
case http.MethodPost:
C.JSONChttp.StatusoK,gin.H{"method":"POST"})
//...

NoRoute表示没有匹配到走这里

1
2
3
4
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})

路由组 BaseUrl

为了区分,加一些公共前缀,如/user/account,/user/logout的user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
r := gin.Default()
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})

}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
r.Run()
}

路由组也是支持嵌套的,例如:

1
2
3
4
5
6
7
8
9
10
11
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {...})
// /shop/xx/oo
}

Gin 中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook) 函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等

定义中间件

Gin中的中间件必须是一个gin.HandlerFunc类型。

例子:记录接口耗时的中间件

例如我们像下面的代码一样定义一个统计请求耗时的中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序
c.Next()
// 如果不加那么他会直接停止计时。

// 阻止调用该请求的剩余处理程序,【但会执行本函数剩下的】,如果本函数剩下的都不想执行,需要额外加入return
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}

Next就类比于深度递归DFS

注册中间件

如何注册,直接加载相应里面就行

1
r.GET("/test2", handleFuc....)

handleFuc就是中间件,其实我们写的处理响应的函数就是一个中间件。可以传递多个handleFuc,会按照顺序执行多个handleFuc
Gin中间件

全局注册

如果需要全局注册中间件,其会第一个执行

1
r.Use(中间件名,中间件名...)

路由组注册

注册StatCost()

写法1:

1
2
3
4
5
6
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}

写法2:

1
2
3
4
5
6
7
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}

获取中间件上下文值

在中间件中,我们可以用下面的函数传递值

1
c.Set("name222", "小王子222") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值

用下面的函数接收中间件的传递

1
2
3
4
5
6
7
8
r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name222").(string)
// 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})

注意事项

默认中间件

gin.Default()默认使用了LoggerRecovery中间件,其中:

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release
  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

即,例如中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序



go func XX(c.Copy())// 该函数里面不能使用C的引用,而是必须用c的拷贝,或者你只能读取他




c.Next()
// 如果不加那么他会直接停止计时。

// 阻止调用该请求的剩余处理程序,【但会执行本函数剩下的】,如果本函数剩下的都不想执行,需要额外加入return
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}

运行多个服务

我们可以在多个端口启动服务,例如:

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

import (
"log"
"net/http"
"time"

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

var (
g errgroup.Group
)

func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})

return e
}

func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})

return e
}

func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
// 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
g.Go(func() error {
return server01.ListenAndServe()
})

g.Go(func() error {
return server02.ListenAndServe()
})

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