Gin 支持各种响应数据类型:JSON、XML、HTML、YAML、Text 等等。响应数据需要使用到 gin.Context 类型。gin.Context 类型的作用有:
- 获取请求数据,包括请求头、Query 参数、Form 数据、Path 参数、请求体等。
- 响应管理,包括设置 HTTP 状态码、编写响应体、设置响应头等。
- 中间件支持。
Context可以携带当前处理函数的信息传递到下一个处理函数,直到达到最终的处理函数。在中间件中可以使用Context读取、修改Context的内容或终止请求处理流程。 - Cookie 操作。
- 读写请求和响应体的原始字节流,以此来处理自定义协议或二进制数据传输。
- 错误处理。可以记录错误并中断请求处理流程。
响应数据示例:
1func main() {
2 r := gin.Default()
3
4 r.GET("/hello", func(ctx *gin.Context) {
5 ctx.JSONP(http.StatusOK, Response{
6 Code: http.StatusOK,
7 Date: nil,
8 })
9 })
10
11 err := r.Run()
12 if err != nil {
13 logrus.Error(err)
14 }
15}
常见的响应数据方法有:
1// String writes the given string into the response body.
2func (c *Context) String(code int, format string, values ...any) {
3 c.Render(code, render.String{Format: format, Data: values})
4}
5
6// JSON serializes the given struct as JSON into the response body.
7// It also sets the Content-Type as "application/json".
8func (c *Context) JSON(code int, obj any) {
9 c.Render(code, render.JSON{Data: obj})
10}
11
12// PureJSON serializes the given struct as JSON into the response body.
13// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
14func (c *Context) PureJSON(code int, obj any) {
15 c.Render(code, render.PureJSON{Data: obj})
16}
17
18// XML serializes the given struct as XML into the response body.
19// It also sets the Content-Type as "application/xml".
20func (c *Context) XML(code int, obj any) {
21 c.Render(code, render.XML{Data: obj})
22}
23
24// YAML serializes the given struct as YAML into the response body.
25func (c *Context) YAML(code int, obj any) {
26 c.Render(code, render.YAML{Data: obj})
27}
28
29// JSONP serializes the given struct as JSON into the response body.
30// It adds padding to response body to request data from a server residing in a different domain than the client.
31// It also sets the Content-Type as "application/javascript".
32func (c *Context) JSONP(code int, obj any) {
33 callback := c.DefaultQuery("callback", "")
34 if callback == "" {
35 c.Render(code, render.JSON{Data: obj})
36 return
37 }
38 c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
39}
40
41// Data writes some data into the body stream and updates the HTTP code.
42func (c *Context) Data(code int, contentType string, data []byte) {
43 c.Render(code, render.Data{
44 ContentType: contentType,
45 Data: data,
46 })
47}
48
49// HTML renders the HTTP template specified by its file name.
50// It also updates the HTTP code and sets the Content-Type as "text/html".
51// See http://golang.org/doc/articles/wiki/
52func (c *Context) HTML(code int, name string, obj any) {
53 instance := c.engine.HTMLRender.Instance(name, obj)
54 c.Render(code, instance)
55}
通过上述响应数据方法的定义可以发现,它们都是调用了 Render() 这个方法。
响应 Text 类型数据
1r := gin.Default()
2
3r.GET("/hello", func(ctx *gin.Context) {
4 // 响应 Text 类型数据
5 ctx.String(http.StatusOK, "Hello World!")
6})
其中 http.StatusOK 是 net/http 包中 200 响应状态码常量。
响应 XML 类型数据
响应和渲染 XML 类型数据可以使用 ctx.XML() 方法:
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.XML(http.StatusOK, gin.H{"message": "Hello World!", "status": http.StatusOK})
3})
响应结果如下:
1<map>
2 <message>
3 Hello World!
4 </message>
5 <status>
6 200
7 </status>
8</map>
其中,ctx.XML() 方法的参数 2 是渲染 XML 的数据对象。其类型为 any,定义如下:
1type any = interface{}
使用 any 可以接收任意类型的数据。
gin.H 是 map 类型,其定义如下:
1type H map[string]any
响应 HTML 类型数据
方式 1:使用 ctx.Header() 和 ctx.String() 方法:
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.Header("Content-Type", "text/html; charset=utf-8")
3 ctx.String(http.StatusOK, "<h2>Hello World!</h2>")
4})
方式 2:ctx.HTML() 方法:
1// 从 templates 目录中加载所有的 HTML 模板文件
2r.LoadHTMLGlob("templates/*")
3
4r.GET("/hello", func(ctx *gin.Context) {
5 ctx.HTML(http.StatusOK, "index.html", nil)
6})
在使用 ctx.HTML() 方法之前,必须先加载 HTML 模板文件。加载 HTML 模板文件的方式有:
-
按文件名称加载:
1r.LoadHTMLFiles("templates/index.html", "templates/welcome.html") -
按路径配对表达式加载:
1r.LoadHTMLGlob("templates/*")
HTML 渲染
Gin 支持对 HTML 模板进行渲染。
例如 templates/welcome.html,其内容如下:
1<!doctype html>
2<html lang="zh">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport"
6 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
7 <meta http-equiv="X-UA-Compatible" content="ie=edge">
8 <title>Welcome!</title>
9</head>
10<body>
11 <h2>{{ .name }}, Welcome!</h2>
12</body>
13</html>
其中 {{ .name }} 表示将 name 属性中的数据渲染于此。
然后编写一个路由:
1r.LoadHTMLFiles("templates/welcome.html")
2
3r.GET("/welcome", func(ctx *gin.Context) {
4 ctx.HTML(http.StatusOK, "welcome.html", gin.H{
5 "name": "张三",
6 })
7})
ctx.HTML() 方法的第 3 个参数就是要渲染到 HTML 模板中的数据对象。
访问 GET /welcome,获取到的内容如下:
1<!doctype html>
2<html lang="zh">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport"
6 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
7 <meta http-equiv="X-UA-Compatible" content="ie=edge">
8 <title>Welcome!</title>
9</head>
10<body>
11 <h2>张三, Welcome!</h2>
12</body>
13</html>
渲染多个模板
在项目目录下创建 templates/index.html:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>首页</title>
6</head>
7<body>
8 <p>网站首页: {{ .data }}</p>
9</body>
10</html>
创建 templates/login.html:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>登录</title>
6</head>
7<body>
8 <p>登录页: {{ .data }}</p>
9</body>
10</html>
接着在 Golang 中加载:
1r := gin.Default()
2// 加载 templates 目录下所有的页面模板(需要在 gin.Default() 后立即调用)
3r.LoadHTMLGlob("templates/*")
4r.GET("/index", func(ctx *gin.Context) {
5 // 渲染数据
6 ctx.HTML(http.StatusOK, "index.html", gin.H{
7 "data": "渲染数据",
8 })
9})
10
11r.GET("/login", func(ctx *gin.Context) {
12 // 渲染数据
13 ctx.HTML(http.StatusOK, "login.html", gin.H{
14 "data": "渲染数据",
15 })
16})
17
18err := r.Run()
19if err != nil {
20 logrus.Error(err)
21}
渲染多层页面
使用 r.LoadHTMLGlob("templates/*") 只能渲染 templates 目录下的 HTML 文件,如果在 templates 目录下建立多层目录,编译时会报错。要渲染多层页面需要使用以下方式:
重新创建一个 templates,创建 templates/login/index.html 和 templates/home/index.html。
templates/login/index.html:
1<!-- 通过 define 给模板指定名称,define 和 end 需成对出现 -->
2{{ define "login/index.html" }}
3
4<!DOCTYPE html>
5<html lang="en">
6<head>
7 <meta charset="UTF-8">
8 <title>登录</title>
9</head>
10<body>
11 <p>登录页面:{{ .data }}</p>
12</body>
13</html>
14
15{{ end }}
templates/home/index.html:
1{{ define "home/index.html" }}
2
3<!DOCTYPE html>
4<html lang="en">
5<head>
6 <meta charset="UTF-8">
7 <title>首页</title>
8</head>
9<body>
10 <p>网站首页: {{ .data }}</p>
11</body>
12</html>
13
14{{ end }}
main.go:
1r := gin.Default()
2// 只能加载 templates 下一级目录的页面模板
3// 当需要通配目录时,使用的是 “**”,通配页面模板文件仅需 “*”
4r.LoadHTMLGlob("templates/**/*")
5
6r.GET("/login", func(ctx *gin.Context) {
7 ctx.HTML(http.StatusOK, "login/index.html", gin.H{
8 "data": "请登录",
9 })
10})
11
12r.GET("/home", func(ctx *gin.Context) {
13 ctx.HTML(http.StatusOK, "home/index.html", gin.H{
14 "data": "渲染数据",
15 })
16})
17
18err := r.Run()
19if err != nil {
20 logrus.Error(err)
21}
响应 YAML 类型数据
响应和渲染 YAML 类型数据可以使用 ctx.YAML() 方法。其使用方式与 ctx.XML() 相同:
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.YAML(http.StatusOK, gin.H{"message": "Hello World!", "status": http.StatusOK})
3})
其结果如下:
1message: Hello World!
2status: 200
响应 JSON 类型数据
响应 JSON 数据有多种方式:
ctx.JSON()ctx.AsciiJSON()ctx.PureJSON()ctx.SecureJSON()
ctx.JSON
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.JSON(http.StatusOK, gin.H{
3 "message": "<h2>你好,世界!</h2>",
4 "status": 200,
5 })
6})
其结果如下:
1{
2 "message": "\u003ch2\u003e你好,世界!\u003c/h2\u003e",
3 "status": 200
4}
ctx.JSON()会使用 Unicode 替换特殊 HTML 字符。
常见的 ctx.JSON() 用法如下:
1// 通常情况下可以使用 map[string]interface{} 传递
2r.GET("/json1", func(ctx *gin.Context) {
3 // 由于 any 的定义是 interface{} 所以也可以写成 map[string]any
4 ctx.JSON(http.StatusOK, map[string]interface{}{
5 "code": http.StatusOK,
6 "data": "Hello Gin!",
7 })
8})
9
10// Gin 给 map[string]interface{} 提供了一个简便的类型定义 gin.H
11r.GET("/json2", func(ctx *gin.Context) {
12 ctx.JSON(http.StatusOK, gin.H{
13 "code": http.StatusOK,
14 "data": "Hello Gin!",
15 })
16})
17
18// ctx.JSON 也支持传入结构体类型实例
19type Response struct {
20 Code uint8
21 Data interface{}
22}
23r.GET("/json3", func(ctx *gin.Context) {
24 ctx.JSON(http.StatusOK, Response{
25 Code: http.StatusOK,
26 Data: "Hello Gin!",
27 })
28})
ctx.AsciiJSON
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.AsciiJSON(http.StatusOK, gin.H{
3 "message": "<h2>你好,世界!</h2>",
4 "status": 200,
5 })
6})
响应结果为:
1{
2 "message": "\u003ch2\u003e\u4f60\u597d\uff0c\u4e16\u754c!\u003c/h2\u003e",
3 "status": 200
4}
ctx.AsciiJSON() 即为 ASCII-only JSON,它会将非 ASCII 标准字符进行 Unicode 转义。它同样会使用 Unicode 替换特殊 HTML 字符。
ctx.PureJSON
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.PureJSON(http.StatusOK, gin.H{
3 "message": "<h2>你好,世界!</h2>",
4 "status": 200,
5 })
6})
1{
2 "message": "<h2>你好,世界!</h2>",
3 "status": 200
4}
ctx.PureJSON() 与上方两个方法不同的是,它不会对 JSON 串进行任何转义,而是直接将它按照原数据输出。
JSON 劫持
JSON 劫持是 XSS 攻击的一种形式,它发生在一个恶意用户能够插入自己的 JavaScript 代码到 JSON 响应中,从而在用户的浏览器上执行非法的脚本。
例如,一个 HTML 页面将请求后的结果插入到页面标签中:
1<!doctype html>
2<html lang="zh">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport"
6 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
7 <meta http-equiv="X-UA-Compatible" content="ie=edge">
8 <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
9 <title>Hello!</title>
10</head>
11<body>
12 <h2>Hello World!</h2>
13</body>
14<script>
15 $.ajax({
16 type: 'GET',
17 url: 'http://localhost:8080/hello',
18 dataType: 'json',
19 success: function(date) {
20 $('h2').html(JSON.stringify(date, null, 2))
21 }
22 })
23</script>
24</html>
假设 GET /hello``GET /hello 请求响应的 message 中包含了非法的脚本代码:
1r.GET("/hello", func(ctx *gin.Context) {
2 messages := []string{
3 "Hello!", "Hi!", "Welcome!",
4 "<script>alert('You have been hacked!')</script>",
5 }
6 ctx.JSON(http.StatusOK, messages)
7})
GET /hello 请求响应成功后,alert('You have been hacked!') 这部分代码将会被执行:
ctx.SecureJSON
ctx.SecureJSON() 能防止 JSON 劫持。如果给定的结构是数组值,则默认预置 "while(1);" 到响应体。
1r.GET("/hello", func(ctx *gin.Context) {
2 messages := []string{
3 "Hello!", "Hi!", "Welcome!",
4 "<script>alert('You have been hacked!')</script>",
5 }
6 ctx.SecureJSON(http.StatusOK, messages)
7})
注:
ctx.SecureJSON()并不能彻底防范 XSS 攻击。
Struct 的 JSON 序列化
由于 ctx.JSON() 等方法,的数据参数 obj 是 any 类型的,因此可以传入自定义的类型的实例。例如:
1type User struct {
2 Id uint64
3 Username string
4 Sex uint8
5}
6
7func main() {
8 // ...
9 r.GET("/user/info", func(ctx *gin.Context) {
10 ctx.JSON(http.StatusOK, User{Id: 123, Username: "zhangsan", Sex: 1})
11 })
12 // ...
13}
发送请求:
1curl -X GET 'http://127.0.0.1:8080/user/info
结果为:
1{
2 "Id": 123,
3 "Username": "zhangsan",
4 "Sex": 1
5}
由于 Golang 结构体字段必须得首字母大写,才能在其它包中访问。所以,要序列化的结构体字段,其首字母必须得是大写的。但这也导致了序列化后的 JSON 串,字段首字母也同样是大写的。为此,可以通过为结构体字段指定 Tags 来设置 JSON 序列化后的字段名称,例如:
1type User struct {
2 Id uint64 `json:"id"`
3 Username string `json:"username"`
4 Sex uint8 `json:"sex"`
5}
再次执行请求,结果如下所示:
1{
2 "id": 123,
3 "username": "zhangsan",
4 "sex": 1
5}
响应字节数据
通过 Context.Data() 方法可以往 ResponseBody 中写入字节数据。例如:
1r.GET("/favicon", func(ctx *gin.Context) {
2 favicon, err := os.Open("./static/favicon.ico")
3 if err != nil {
4 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
5 return
6 }
7 // 结束时关闭文件流
8 defer func(file multipart.File) {
9 if err := file.Close(); err != nil {
10 logrus.Error(err)
11 }
12 }(favicon)
13
14 // 获取字节数据
15 bytes, err := io.ReadAll(favicon)
16 if err != nil {
17 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
18 return
19 }
20
21 // 假设对文件进行了一些操作...
22
23 // 响应字节数据
24 ctx.Data(http.StatusOK, "application/octet-stream", bytes)
25})
也可以通过 Context.Writer.Write() 方法分次数往 ResponseBody 中写入字节数据。例如:
1r.GET("/favicon", func(ctx *gin.Context) {
2 favicon, err := os.Open("./static/favicon.ico")
3 if err != nil {
4 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
5 return
6 }
7 // 结束时关闭文件流
8 defer func(file multipart.File) {
9 if err := file.Close(); err != nil {
10 logrus.Error(err)
11 }
12 }(favicon)
13
14 // 使用一个缓冲区来逐块读取和响应数据
15 buffer := make([]byte, 1024)
16
17 // 循环读取数据并写入响应,每次最多读取 1024 byte 数据
18 for {
19 size, err := favicon.Read(buffer)
20 if err == io.EOF {
21 break // 读取到数据流结尾,结束循环
22 } else if err != nil {
23 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
24 return
25 }
26
27 // 将读取的数据写入响应
28 if _, writeErr := ctx.Writer.Write(buffer[:size]); writeErr != nil {
29 _ = ctx.AbortWithError(http.StatusInternalServerError, writeErr)
30 return
31 }
32 }
33 ctx.Header("Content-Type", "application/octet-stream")
34})
静态文件
静态文件服务使用的是 IRoutes(或 RouterGroup)中的 Static 中开头的方法进行绑定。
-
挂载目录:
-
RouterGroup.Static():1r.Static("/static", "./static")当访问
GET /static时,默认会访问到挂载目录下的index.html文件。假设static目录中有welcome.html文件,可以通过GET /static/welcome.html访问到该文件。 -
RouterGroup.StaticFS():1r.StaticFS("/static", http.Dir("./static"))或:
1type MyFileSystem struct{} 2 3// 根据实际情况实现一个 Open(string) (http.File, error) 接口 4func (*MyFileSystem) Open(name string) (file http.File, err error) { 5 file, err = os.Open(path.Join("./static", name)) 6 if err != nil { 7 logrus.Error(err) 8 panic(err) 9 } 10 return 11} 12 13var fs = new(MyFileSystem) 14 15r.StaticFS("/static", fs)
-
-
挂载文件:
-
RouterGroup.StaticFile():1r.StaticFile("/home", "./static/index.html")
-
评论