Dawn's Blogs

分享技术 记录成长

0%

Gin基本使用 (2) 渲染和获取参数

Gin渲染

HTML渲染

首先编写模板文件index.tmpl

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>

Gin中使用LoadHTMLGlob()或者LoadHTMLFiles()进行模板渲染:

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"
"net/http"
)

func main() {
r := gin.Default()
// 模板解析
r.LoadHTMLFiles("templates/index.tmpl")

r.GET("/index", func(c *gin.Context) {
// 模板渲染
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Dawn ZH",
})
})
// 运行
r.Run(":9090")
}

使用不同目录下的名称相同模板:可以指定模板的父文件夹文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})
})
router.GET("/users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})
})
router.Run(":8080")
}

自定义模板函数

定义一个safe函数,不转义HTML标签:

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

import (
"github.com/gin-gonic/gin"
"html/template"
"net/http"
)

func main() {
r := gin.Default()
// 添加自定义函数safe
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})
// 模板解析
r.LoadHTMLFiles("index.tmpl")

r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Hello, DawnZH",
"href": "<a href='http://dawnzzz.github.io'>Dawn的博客地址</a>",
})
})
r.Run(":9090")
}

在模板中使用自定义函数safe

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
{{.href | safe}}
</body>
</html>

也可以自定义 HTML 模板渲染器:

1
2
3
4
5
6
7
8
import "html/template"

func main() {
router := gin.Default()
html := template.Must(template.ParseFiles("file1", "file2"))
router.SetHTMLTemplate(html)
router.Run(":8080")
}

自定义分隔符:

1
2
3
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates")

静态文件处理

当渲染的HTML文件中使用了静态文件时,只需要在模板解析前调用Static方法即可:

1
2
3
4
5
6
r := gin.Default()
// 引用静态文件
// HTML文件中的/xxx路径被替换为./static路径
r.Static("/xxx", "./static")
// 模板解析
r.LoadHTMLFiles("index.tmpl")

在模板中引用静态文件:

1
<link rel="stylesheet" href="xxx/index.css">

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
33
34
35
36
37
package main

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

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

r.GET("/json1", func(c *gin.Context) {
// 方法1:使用map
// H is a shortcut for map[string]interface{}
data := gin.H{
"name": "Dawn",
"content": "Hello world",
"age": 18,
}

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

r.GET("/json2", func(c *gin.Context) {
// 方法2:结构体
var data struct {
Name string
Content string
Age int
}
data.Name = "DawnZH"
data.Content = "Hello Dawn"
data.Age = 22
c.JSON(http.StatusOK, data)
})

r.Run(":9090")
}

通常,JSON 使用 unicode 替换特殊 HTML 字符,例如 < 变为 \u003c。如果要按字面对这些字符进行编码,则可以使用 PureJSON。

使用 SecureJSON 防止 json 劫持。如果给定的结构是数组值,则默认预置 "while(1)," 到响应体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
r := gin.Default()

// 你也可以使用自己的 SecureJSON 前缀
// r.SecureJsonPrefix(")]}',\n")

r.GET("/someJSON", func(c *gin.Context) {
names := []string{"lena", "austin", "foo"}

// 将输出:while(1);["lena","austin","foo"]
c.SecureJSON(http.StatusOK, names)
})

// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}

获取参数

获取querystring

querystring指URL中?后面携带的参数,获取querystring的方法,可以使用Query方法、DefaultQueryGetQuery方法。

DefaultQuery在没有获取到参数时会设置默认值,GetQuery可以判断是否取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
r.GET("/index", func(c *gin.Context) {
// 获取querystring
// 方式1:
// data := c.Query("query")
// 方式2:
// data := c.DefaultQuery("query", "default value")
// 方式3:
data, ok := c.GetQuery("query")
if !ok {
// 没取到
// ...
}
c.JSON(http.StatusOK, gin.H{
"data": data,
})
})

获取form表单

获取POST请求参数时,与获取GET请求参数相类似,也有三个方法:PostFormDefaultPostForm以及GetPostForm

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() {
r := gin.Default()
r.LoadHTMLFiles("./form.html", "./index.html")

r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "form.html", nil)
})

r.POST("/login", func(c *gin.Context) {
// 方法1:
user := c.PostForm("username")
pwd := c.PostForm("password")
// 方法2:
// user, ok := c.GetPostForm("username")
// 其他操作
c.HTML(http.StatusOK, "index.html", gin.H{
"username": user,
"password": pwd,
})
})

r.Run(":9090")
}

获取path参数(URI)

获取请求URL路径中的参数的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
router := gin.Default()

// 此 handler 将匹配 /user/john 但不会匹配 /user/ 或者 /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})

// 此 handler 将匹配 /user/john/ 和 /user/john/send
// 如果没有其他路由匹配 /user/john,它将重定向到 /user/john/
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)
})

router.Run(":8080")
}

参数绑定

基于请求的Content-type失败请求中的类型并自动提取请求中的querystringformjsonuri等参数到结构体中。ShouldBind能够自动提取jsonformquerystring类型的数据并放入结构体中。

Gin使用 go-playground/validator/v10 进行验证。 查看标签用法的全部文档

使用时,需要在要绑定的所有字段上,设置相应的tag。 例如,使用 JSON 绑定时,设置字段标签为 json:"fieldname"

Gin提供了两类绑定方法:

  • Must bind:
    • Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML
    • Behavior - 这些方法属于 MustBindWith 的具体调用。 如果发生绑定错误,则请求终止,并触发 c.AbortWithError(400, err).SetType(ErrorTypeBind)。响应状态码被设置为 400 并且 Content-Type 被设置为 text/plain; charset=utf-8。 如果在此之后尝试设置响应状态码,Gin会输出日志 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422如果希望更好地控制绑定,考虑使用 ShouldBind 等效方法。
  • Should bind:
    • Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    • Behavior - 这些方法属于 ShouldBindWith 的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求

使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定如果明确知道要绑定什么,可以使用 MustBindWithShouldBindWith

也可以指定必须绑定的字段。 如果一个字段的 tag 加上了 binding:"required",但绑定时是空值,Gin 会报错。

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

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

type userInfo struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}

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

// 获取querystring
r.GET("/query", func(c *gin.Context) {
var u userInfo
err := c.ShouldBind(&u)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "Bad",
})
} else {
c.JSON(http.StatusOK, gin.H{
"status": "OK",
})
}
})

// 获取form表单
r.POST("/form", func(c *gin.Context) {
var u userInfo
err := c.ShouldBind(&u)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "Bad",
})
} else {
c.JSON(http.StatusOK, gin.H{
"status": "OK",
})
}
})

// 获取json
r.POST("/json", func(c *gin.Context) {
var u userInfo
err := c.ShouldBind(&u)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status": "Bad",
})
} else {
c.JSON(http.StatusOK, gin.H{
"status": "OK",
})
}
})

r.Run(":9090")
}

将 request body 绑定到不同结构体中

一般通过调用 c.Request.Body 方法绑定数据,但不能多次调用这个方法,在第二次调用时 因为 c.Request.Body 是 EOF,所以会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type formA struct {
Foo string `json:"foo" xml:"foo" binding:"required"`
}

type formB struct {
Bar string `json:"bar" xml:"bar" binding:"required"`
}

func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// c.ShouldBind 使用了 c.Request.Body,不可重用。
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// 因为现在 c.Request.Body 是 EOF,所以这里会报错。
} else if errB := c.ShouldBind(&objB); errB == nil {
c.String(http.StatusOK, `the body should be formB`)
} else {
...
}
}

要想多次绑定,可以使用 c.ShouldBindBodyWith

1
2
3
4
5
6
7
8
9
10
11
12
13
14
objA := formA{}
objB := formB{}
// 读取 c.Request.Body 并将结果存入上下文。
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// 这时, 复用存储在上下文中的 body。
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// 可以接受其他格式
} else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
} else {
...
}
  • c.ShouldBindBodyWith 会在绑定之前将 body 存储到上下文中。 这会对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。
  • 只有某些格式需要此功能,如 JSON, XML, MsgPack, ProtoBuf。 对于其他格式,如 Query, Form, FormPost, FormMultipart 可以多次调用 c.ShouldBind() 而不会造成任任何性能损失(因为它们使用 req.Form 或 url.Values,而不是 req.Body)。

绑定 uri

也可以将 URI 中的变量绑定到结构体中,使用 uri tag。

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

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

type Person struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}

func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
route.Run(":8088")
}

设置和获取Cookie

使用 Cookie 函数可以获取 Cookie,使用 setCookie 函数可以设置 Cookie:

1
2
3
4
5
6
7
8
9
10
11
router.GET("/cookie", func(c *gin.Context) {

cookie, err := c.Cookie("gin_cookie")

if err != nil {
cookie = "NotSet"
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}

fmt.Printf("Cookie value: %s \n", cookie)
})