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.StatusOK
是 net/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.H
是 map
类型,其定义如下:
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 模板文件的方式有:
-
按文件名称加载:
r.LoadHTMLFiles("templates/index.html", "templates/welcome.html")
-
按路径配对表达式加载:
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 数据有多种方式:
ctx.JSON()
ctx.AsciiJSON()
ctx.PureJSON()
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!')
这部分代码将会被执行:
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()
等方法,的数据参数 obj
是 any
类型的,因此可以传入自定义的类型的实例。例如:
type User struct {
Id uint64
Username string
Sex uint8
}
func main() {
// ...
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
}
响应字节数据
通过 Context.Data()
方法可以往 ResponseBody
中写入字节数据。例如:
r.GET("/favicon", func(ctx *gin.Context) {
favicon, err := os.Open("./static/favicon.ico")
if err != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
// 结束时关闭文件流
defer func(file multipart.File) {
if err := file.Close(); err != nil {
logrus.Error(err)
}
}(favicon)
// 获取字节数据
bytes, err := io.ReadAll(favicon)
if err != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
// 假设对文件进行了一些操作...
// 响应字节数据
ctx.Data(http.StatusOK, "application/octet-stream", bytes)
})
也可以通过 Context.Writer.Write()
方法分次数往 ResponseBody
中写入字节数据。例如:
r.GET("/favicon", func(ctx *gin.Context) {
favicon, err := os.Open("./static/favicon.ico")
if err != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
// 结束时关闭文件流
defer func(file multipart.File) {
if err := file.Close(); err != nil {
logrus.Error(err)
}
}(favicon)
// 使用一个缓冲区来逐块读取和响应数据
buffer := make([]byte, 1024)
// 循环读取数据并写入响应,每次最多读取 1024 byte 数据
for {
size, err := favicon.Read(buffer)
if err == io.EOF {
break // 读取到数据流结尾,结束循环
} else if err != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
// 将读取的数据写入响应
if _, writeErr := ctx.Writer.Write(buffer[:size]); writeErr != nil {
_ = ctx.AbortWithError(http.StatusInternalServerError, writeErr)
return
}
}
ctx.Header("Content-Type", "application/octet-stream")
})
静态文件
静态文件服务使用的是 IRoutes
(或 RouterGroup
)中的 Static
中开头的方法进行绑定。
-
挂载目录:
-
RouterGroup.Static()
:r.Static("/static", "./static")
当访问
GET /static
时,默认会访问到挂载目录下的index.html
文件。假设static
目录中有welcome.html
文件,可以通过GET /static/welcome.html
访问到该文件。 -
RouterGroup.StaticFS()
:r.StaticFS("/static", http.Dir("./static"))
或:
type MyFileSystem struct{} // 根据实际情况实现一个 Open(string) (http.File, error) 接口 func (*MyFileSystem) Open(name string) (file http.File, err error) { file, err = os.Open(path.Join("./static", name)) if err != nil { logrus.Error(err) panic(err) } return } var fs = new(MyFileSystem) r.StaticFS("/static", fs)
-
-
挂载文件:
-
RouterGroup.StaticFile()
:r.StaticFile("/home", "./static/index.html")
-
评论