Gin 支持各种响应数据类型:JSON、XML、HTML、YAML、Text 等等。响应数据需要使用到 gin.Context 类型。gin.Context 类型的作用有:

  • 获取请求数据,包括请求头、Query 参数、Form 数据、Path 参数、请求体等。
  • 响应管理,包括设置 HTTP 状态码、编写响应体、设置响应头等。
  • 中间件支持。Context 可以携带当前处理函数的信息传递到下一个处理函数,直到达到最终的处理函数。在中间件中可以使用 Context 读取、修改 Context 的内容或终止请求处理流程。
  • Cookie 操作。
  • 读写请求和响应体的原始字节流,以此来处理自定义协议或二进制数据传输。
  • 错误处理。可以记录错误并中断请求处理流程。

响应 Text 类型数据

r := gin.Default()

r.GET("/hello", func(ctx *gin.Context) {
  // 响应 Text 类型数据
  ctx.String(http.StatusOK, "Hello World!")
})

其中 http.StatusOKnet/http 包中 200 响应状态码常量。


响应 XML 类型数据

响应和渲染 XML 类型数据可以使用 ctx.XML() 方法:

r.GET("/hello", func(ctx *gin.Context) {
  ctx.XML(http.StatusOK, gin.H{"message": "Hello World!", "status": http.StatusOK})
})

响应结果如下:

<map>
    <message>
        Hello World!
    </message>
    <status>
        200
    </status>
</map>

其中,ctx.XML() 方法的参数 2 是渲染 XML 的数据对象。其类型为 any,定义如下:

type any = interface{}

使用 any 可以接收任意类型的数据。

gin.Hmap 类型,其定义如下:

type H map[string]any

响应 HTML 类型数据

方式 1:使用 ctx.Header()ctx.String() 方法:

r.GET("/hello", func(ctx *gin.Context) {
  ctx.Header("Content-Type", "text/html; charset=utf-8")
  ctx.String(http.StatusOK, "<h2>Hello World!</h2>")
})

方式 2:ctx.HTML() 方法:

// 从 templates 目录中加载所有的 HTML 模板文件
r.LoadHTMLGlob("templates/*")

r.GET("/hello", func(ctx *gin.Context) {
  ctx.HTML(http.StatusOK, "index.html", nil)
})

在使用 ctx.HTML() 方法之前,必须先加载 HTML 模板文件。加载 HTML 模板文件的方式有:

  1. 按文件名称加载:

    r.LoadHTMLFiles("templates/index.html", "templates/welcome.html")
    
  2. 按路径配对表达式加载:

    r.LoadHTMLGlob("templates/*")
    

HTML 渲染

Gin 支持对 HTML 模板进行渲染。

例如 templates/welcome.html,其内容如下:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Welcome!</title>
</head>
<body>
    <h2>{{ .name }}, Welcome!</h2>
</body>
</html>

其中 {{ .name }} 表示将 name 属性中的数据渲染于此。

然后编写一个路由:

r.LoadHTMLFiles("templates/welcome.html")

r.GET("/welcome", func(ctx *gin.Context) {
  ctx.HTML(http.StatusOK, "welcome.html", gin.H{
    "name": "张三",
  })
})

ctx.HTML() 方法的第 3 个参数就是要渲染到 HTML 模板中的数据对象。

访问 GET /welcome,获取到的内容如下:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Welcome!</title>
</head>
<body>
    <h2>张三, Welcome!</h2>
</body>
</html>

响应 YAML 类型数据

响应和渲染 YAML 类型数据可以使用 ctx.YAML() 方法。其使用方式与 ctx.XML() 相同:

r.GET("/hello", func(ctx *gin.Context) {
  ctx.YAML(http.StatusOK, gin.H{"message": "Hello World!", "status": http.StatusOK})
})

其结果如下:

message: Hello World!
status: 200

响应 JSON 类型数据

响应 JSON 数据有多种方式:

  1. ctx.JSON()
  2. ctx.AsciiJSON()
  3. ctx.PureJSON()
  4. ctx.SecureJSON()

ctx.JSON

r.GET("/hello", func(ctx *gin.Context) {
  ctx.JSON(http.StatusOK, gin.H{
    "message": "<h2>你好,世界!</h2>",
    "status":  200,
  })
})

其结果如下:

{
  "message": "\u003ch2\u003e你好,世界!\u003c/h2\u003e",
  "status": 200
}

ctx.JSON() 会使用 Unicode 替换特殊 HTML 字符。

ctx.AsciiJSON

r.GET("/hello", func(ctx *gin.Context) {
  ctx.AsciiJSON(http.StatusOK, gin.H{
    "message": "<h2>你好,世界!</h2>",
    "status":  200,
  })
})

响应结果为:

{
  "message": "\u003ch2\u003e\u4f60\u597d\uff0c\u4e16\u754c!\u003c/h2\u003e",
  "status": 200
}

ctx.AsciiJSON() 即为 ASCII-only JSON,它会将非 ASCII 标准字符进行 Unicode 转义。它同样会使用 Unicode 替换特殊 HTML 字符。

ctx.PureJSON

r.GET("/hello", func(ctx *gin.Context) {
  ctx.PureJSON(http.StatusOK, gin.H{
    "message": "<h2>你好,世界!</h2>",
    "status":  200,
  })
})
{
  "message": "<h2>你好,世界!</h2>",
  "status": 200
}

ctx.PureJSON() 与上方两个方法不同的是,它不会对 JSON 串进行任何转义,而是直接将它按照原数据输出。

JSON 劫持

JSON 劫持是 XSS 攻击的一种形式,它发生在一个恶意用户能够插入自己的 JavaScript 代码到 JSON 响应中,从而在用户的浏览器上执行非法的脚本。

例如,一个 HTML 页面将请求后的结果插入到页面标签中:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <title>Hello!</title>
</head>
<body>
	<h2>Hello World!</h2>
</body>
<script>
    $.ajax({
        type: 'GET',
        url: 'http://localhost:8080/hello',
        dataType: 'json',
        success: function(date) {
            $('h2').html(JSON.stringify(date, null, 2))
        }
    })
</script>
</html>

假设 GET /hello``GET /hello 请求响应的 message 中包含了非法的脚本代码:

r.GET("/hello", func(ctx *gin.Context) {
  messages := []string{
    "Hello!", "Hi!", "Welcome!",
    "<script>alert('You have been hacked!')</script>",
  }
  ctx.JSON(http.StatusOK, messages)
})

GET /hello 请求响应成功后,alert('You have been hacked!') 这部分代码将会被执行:

演示 JSON 注入

ctx.SecureJSON

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

r.GET("/hello", func(ctx *gin.Context) {
  messages := []string{
    "Hello!", "Hi!", "Welcome!",
    "<script>alert('You have been hacked!')</script>",
  }
  ctx.SecureJSON(http.StatusOK, messages)
})

注:ctx.SecureJSON() 并不能彻底防范 XSS 攻击。

Struct 的 JSON 序列化

由于 ctx.JSON() 等方法,的数据参数 objany 类型的,因此可以传入自定义的类型的实例。例如:

type User struct {
	Id       uint64
	Username string
	Sex      uint8
}

r.GET("/user/info", func(ctx *gin.Context) {
  ctx.JSON(http.StatusOK, User{Id: 123, Username: "zhangsan", Sex: 1})
})

发送请求:

curl -X GET 'http://127.0.0.1:8080/user/info

结果为:

{
    "Id": 123,
    "Username": "zhangsan",
    "Sex": 1
}

由于 Golang 结构体字段必须得首字母大写,才能在其它包中访问。所以,要序列化的结构体字段,其首字母必须得是大写的。但这也导致了序列化后的 JSON 串,字段首字母也同样是大写的。为此,可以通过为结构体字段指定 Tags 来设置 JSON 序列化后的字段名称,例如:

type User struct {
	Id       uint64 `json:"id"`
	Username string `json:"username"`
	Sex      uint8  `json:"sex"`
}

再次执行请求,结果如下所示:

{
    "id": 123,
    "username": "zhangsan",
    "sex": 1
}