记录Gin官方文档学习过程
Gin 介绍
概述
Gin 是一个使用 go 语言编写的 web 框架,是一个高性能,简洁的框架
文档 | Gin Web Framework (gin-gonic.com)
特性
- 快速:基于 Radix 树的路由,小内存占用,没有反射
- 支持中间件:传入的 HTTP 请求可以由一系列中间件和最终操作来处理。例如:Logger、Authorization、GZIP,最终操作 DB 数据库
- Crash 处理:Gin可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,服务器可以始终可用,例如,可以向 Sentry 报告这个 panic
- Json 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值得存在
- 错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的错误,最终,中间件可以将它们写入日志文件,数据库并通过网络进行发送
- 可扩展性:新建一个中间件
Gin 快速入门
1、更改代理
[root@localhost Go]# export GOPROXY=http://goproxy.cn
2、go mod 并下载安装 gin
[root@localhost Go]# go mod init Go # Go 自定义文件夹
[root@localhost Go]# go get -u github.com/gin-gonic/gin
3、编写 gin.go 文件
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
ginServer := gin.Default()
ginServer.GET("/hello", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{"msg": "hello,world!"})
})
ginServer.Run("8080")
}
4、运行 gin.go
[root@localhost Go]# go run gin.go
发现报错
[GIN-debug] [ERROR] listen tcp: address 8080: missing port in address
// 发现错误
ginServer.Run("8080")
// 更改为
ginServer.Run(":8080")
5、优化访问图标
推荐 ico 图标网址:https://www.iconfinder.com/
Restful 构架
在该框架中,每一个网址代表一种资源,不同的请求方式表示执行不同的操作
GET (SELECT) // 从服务器取出资源
POST (CREATE) // 在服务器新建资源
PUT (UPDATE) // 在服务器更新资源
DELETE (DELETE) // 从服务器删除资源
Gin 使用
1.AsciiJSON
func (c *Context) AsciiJSON(code int, obj any)
router.GET("/", func(ctx *gin.Context) {
ctx.AsciiJSON(http.StatusOK, "hello, 小阳")
})
// "hello, \u5c0f\u9633"
如果同时发送了一个 JSON 响应和一个 HT0ML 响应。根据 HTTP 规范,当一个请求有多个响应时,浏览器会优先处理第一个有效的响应,并且忽略后续的响应,这样就会导致 html 网页变成一个文本
2.LoadHTMLGlob LoadHTMLFiles
HTML 渲染
响应一个页面之前,需要先渲染
LoadHTMLGlob("templates/*")
可以渲染某一目录下的直接子文件
如果有同名文件时,需要传入"templates/**/*"
如果你想加载 "templates" 目录下直接的一级文件以及所有子目录中的文件,你需要使用其他的加载方式,比如结合 LoadHTMLFiles()
和 LoadHTMLGlob()
方法,或者使用一个通配符模式。
func (c *Context) HTML(code int, name string, obj any)
// 加载 html
router.LoadHTMLGlob("templates/**/*")
router.LoadHTMLFiles("templates/index.html")
router.GET("/frontend", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "frontend/index.html", gin.H {
"title": "frontend",
})
})
router.GET("/backend", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "backend/index.html", gin.H {
"title": "backend",
})
})
router.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", gin.H {
"msg": "hello, gin",
})
})
3.pusher.Push
HTTP2 server 推送
HTTP/2 服务器推送是一种优化性能的技术,它允许服务器在客户端请求之前将资源主动推送到客户端,从而减少客户端请求的往返时间
Static 路由映射
// HTML 部分代码
<script src="/assets/app.js"></script> <!-- # 访问路由路径/assets/app.js 映射到本地文件 ./js/app.js -->
// 路由路径与本地文件路径映射
router.Static("/assets", "./js")
Must 简化错误处理
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script> <!-- # 访问路由路径/assets/app.js 映射到本地文件 ./js/app.js -->
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
Must 用于简化模板加载过程中的错误处理,如果发生错误,会将错误传递给 panic,并打印错误信息,停止运行
SetHTMLTemplate 加载指定模板
router.SetHTMLTemplate(html)
这意味着在处理请求时,Gin 将使用指定的 HTML 模板来渲染 HTML 响应。
Pusher 直接推送资源
if pusher := ctx.Writer.Pusher(); pusher != nil {
// 尝试推送 "/js/app.js" 文件
if err := pusher.Push("/js/app.js", nil); err != nil {
log.Printf("Failed to push: %v\n", err)
}
}
如果不加上这段代码,服务器将不会尝试使用 HTTP/2 的服务器推送功能。这意味着当客户端请求根路径 ("/") 时,服务器不会自动将 "/js/app.js" 文件提前推送给客户端。
客户端在接收到 HTML 页面后,会解析其中的内容,并发现需要加载 "/js/app.js" 文件。此时,客户端会发送额外的请求给服务器,请求获取该 JavaScript 文件。服务器会响应这个请求,并返回 "/js/app.js" 文件给客户端。这个过程可能会增加页面加载的时间,因为需要额外的网络往返时间。
总之,不加上这段代码的话,服务器不会主动推送 "/js/app.js" 文件给客户端,而是等待客户端请求时再进行响应。
4.JSONP
使用 JSONP 向不同域的服务器请求数据。如果查询参数存在回调,则将回调添加到响应体中。
router.GET("/", func(ctx *gin.Context) {
var m = map[string]any {
"name": "xiaoxie",
"age": 18,
}
ctx.JSONP(http.StatusOK, m)
})
测试结果
{"age":18,"name":"xiaoxie"}
http://localhost:8080/?callback=xxxxx
xxxxx({"age":18,"name":"xiaoxie"});
5.ShouldBind
Multipart/Urlencoded 绑定
可以使用 ShouldBind
方法来将客户端发送的请求体数据绑定到结构体中,从而进行 Multipart 或 Urlencoded 数据的绑定
router.POST("/login", func(ctx *gin.Context) {
var form LoginForm
if ctx.ShouldBind(&form) == nil {
if form.User == "user" && form.Password == "password" {
ctx.JSON(http.StatusOK, "you are logged in")
} else {
ctx.JSON(401, "failed to log in")
}
}
})
发送 POST请求 测试
[xiaoxie@localhost ~]$ curl -v --form user=user --form password=password http://192.168.1.8:8080/login
* About to connect() to 192.168.1.8 port 8080 (#0)
* Trying 192.168.1.8...
* Connected to 192.168.1.8 (192.168.1.8) port 8080 (#0)
> POST /login HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 192.168.1.8:8080
> Accept: */*
> Content-Length: 248
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=----------------------------f6af752b909b
>
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Mon, 29 Jan 2024 05:28:38 GMT
< Content-Length: 19
<
* Connection #0 to host 192.168.1.8 left intact
"you are logged in"
6.Multipart/Urlencoded
表单
PostForm
是 Gin 框架中用于从 POST 请求的表单数据中获取参数值的方法。它用于获取客户端通过表单 POST 方法提交的数据。DefaultPostForm
可指定默认参数值
Urlencoded 编码
这是另一种常见的表单提交方式,适用于普通的表单数据,不包含文件上传。在这种方式下,表单数据会被编码为键值对,并通过请求体以 URL 编码的形式发送给服务器。这是默认的表单提交方式。
<form action="/submit" method="post" enctype="application/x-www-form-urlencoded">
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<input type="submit" value="Submit">
</form>
Multipart 编码
提交文件,使用 enctype="multipart/form-data"
指定了 multipart/form-data
编码类型
<form action="/upload" method="post" enctype="multipart/form-data">
<label for="file">Choose a file:</label>
<input type="file" id="file" name="file">
<label for="description">Description:</label>
<textarea id="description" name="description"></textarea>
<input type="submit" value="Upload">
</form>
用户可以选择一个文件并输入描述信息,然后提交表单。在服务器端,通过解析 multipart/form-data
编码的请求体,可以获取文件和其他表单字段的数据。
服务器端解析
func main() {
router := gin.Default()
// 使用中间件处理multipart/form-data请求
router.Use(gin.Logger())
router.Use(gin.Recovery())
router.Use(multipartform.Middleware())
router.POST("/upload", func(c *gin.Context) {
// 获取单个文件
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 处理文件,例如保存到磁盘或进行其他处理
// 这里只是简单地打印文件名和大小
fmt.Printf("File Name: %s\n", file.Filename)
fmt.Printf("File Size: %d bytes\n", file.Size)
// 获取其他表单字段的数据
fieldValue := c.PostForm("field_name")
// 返回响应
c.JSON(http.StatusOK, gin.H{
"message": "File uploaded successfully",
"file_name": file.Filename,
"file_size": file.Size,
"field_name": fieldValue,
})
})
// 启动服务器
err := router.Run(":8080")
if err != nil {
log.Fatal(err)
}
}
7.PureJSON
相对于 JSON 可以直接显示字面值,而不是编码值
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(http.StatusOK, gin.H{
"html": "<b>Hello, world!</b>",
})
})
// 访问测试
{"html":"<b>Hello, world!</b>"}
8.Query
和 post form
Query 通常与 HTTP GET 请求一起使用,而 Post Form 通常与 HTTP POST 请求一起使用,用于在 Web 开发中进行数据传递和提交
在Web开发中,"Query"和"Post Form"是两种不同的方式来发送数据到服务器的。它们的区别主要在于数据传输的方式和用途:
-
查询(Query):
- 使用查询字符串将数据附加到URL中
http://example.com/page?name=John&age=30
- 数据以键值对的形式发送,例如:
example.com/page?name=John&age=30
- 通常用于向服务器请求特定资源或执行特定操作,例如搜索查询或筛选数据
- 数据对应的键值对会显示在URL中,因此不适合发送敏感信息
// 可以使用 `gin.Context` 的 `Query` 方法来获取 URL 中的查询参数。例如: func SomeHandler(c *gin.Context) { name := c.Query("name") age := c.Query("age") // 使用获取到的参数进行处理 }
-
POST表单(PostForm):
- 将数据放在HTTP请求的主体中进行发送,而不是作为URL的一部分
- 通常用于向服务器提交数据,例如通过注册表单提交用户信息或上传文件
- 更适合发送大量数据或包含敏感信息的数据,因为它们不会显示在URL中,也不受URL长度限制的影响
// 可以使用 `gin.Context` 的 `PostForm` 方法来获取表单数据。例如: func SomeHandler(c *gin.Context) { name := c.PostForm("name") age := c.PostForm("age") // 使用获取到的表单数据进行处理 }
综上所述,Query
适用于将数据附加到URL以请求特定资源
,而PostForm
适用于向服务器提交数据
,例如通过表单提交用户输入或上传文件。选择使用哪种方式取决于您的具体需求和安全考虑。
9.SecureJSON
使用 SecureJSON 防止 json 劫持。如果给定的结构是数组值,则默认预置 "while(1),"
到响应体
r.GET("/", func(ctx *Context) {
names := []string{"lena", "austin", "foo"}
ctx.SecureJSON(http.StatusOK, names)
})
10.XML/JSON/YAML/ProtoBuf
渲染
XML
r.GET("/someXML", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
// 访问测试
<map>
<message>hey</message>
<status>200</status>
</map>
JSON
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
// 访问测试
{
"message": "hey",
"status": 200
}
YAML
r.GET("/someYAML", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
// 在浏览器访问 someYAML 会直接将该文件下载下来
// someYAML 文件
message: hey
status: 200
ProtoBuf
// 导入 "github.com/gin-gonic/gin/testdata/protoexample"
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// protobuf 的具体定义写在 testdata/protoexample 文件中。
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// 请注意,数据在响应中变为二进制数据
// 将输出被 protoexample.Test protobuf 序列化了的数据
c.ProtoBuf(http.StatusOK, data)
})
// Test 结构体
type Test struct {
Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
// 在浏览器访问 someYAML 会直接将该文件下载下来
// someProtoBuf 文件
test
11.SaveUploadedFile
上传文件
本节列出了上传文件的 api 用法,具体包括同时上传单个文件和同时上传多个文件,并将文件保存到项目当前目录
单文件上传
func main() {
r := gin.Default()
r.LoadHTMLFiles("templates/index.html")
r.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", gin.H{})
})
r.POST("/single", func(ctx *gin.Context) {
file, _ := ctx.FormFile("file")
log.Println(file.Filename)
dst := "./" + file.Filename
ctx.SaveUploadedFile(file, dst)
ctx.String(http.StatusOK, fmt.Sprintf("'%s' uploaded", file.Filename))
})
// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}
// 测试
'新建 Blank Markdown file.md' uploaded
12.DataFromReader
从 Reader 读取数据
r.GET("/", func(ctx *gin.Context) {
response, err := http.Get("https://www.......")
if err != nil || response.StatusCode != http.StatusOK {
ctx.Status(http.StatusServiceUnavailable)
return
}
// 读取 response 中的响应数据
reader := response.Body // 响应数据
contentLength := response.ContentLength // 内容长度
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string {
"Content-Disposition": `attachment; filename="gopher.png"`,
}
ctx.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
13.使用 gin.BasicAuth
中间件
Gin 提供了一个中间件 gin.BasicAuth()
,可以方便地实现这个功能。你可以传递一个函数作为参数,这个函数会接收用户名和密码,然后返回一个布尔值来表示认证是否成功。如果认证失败,Gin 会自动发送一个 401 Unauthorized 响应,通过BasicAuth中间件可以快速实现 http 基础认证,提高应用安全性
// 模拟一些私人数据
00var secrets = gin.H{
"foo": gin.H{"email": "foo@bar.com", "phone": "123433"},
"austin": gin.H{"email": "austin@example.com", "phone": "666"},
"lena": gin.H{"email": "lena@guapa.com", "phone": "523443"},
}
func main() {
r := gin.Default()
// 路由组使用 gin.BasicAuth() 中间件
// gin.Accounts 是 map[string]string 的一种快捷方式
authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"foo": "bar",
"austin": "1234",
"lena": "hello2",
"manu": "4321",
}))
// /admin/secrets 端点
// 触发 "localhost:8080/admin/secrets
authorized.GET("/secrets", func(c *gin.Context) {
// 获取用户,它是由 BasicAuth 中间件设置的
user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
c.JSON(200, gin.H{
"user": user,
"secret": secret,
})
} else {
c.JSON(200, gin.H{
"user": user,
"secret": "No Secret",
})
}
})
r.Run(":8080")
}
14.在中间件中使用 Goroutine
r.GET("/long_async", func(c *gin.Context) {
// 创建在 goroutine 中使用的副本
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
// 请注意您使用的是复制的上下文 "cCp",这一点很重要
log.Println("done! in path " + cCp.Request.URL.Path)
}()
})
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)
// 因为没有使用 goroutine,不需要拷贝上下文
log.Println("done! in path " + c.Request.URL.Path)
})
// 创建在 goroutine 中使用的副本
cCp := c.Copy()
// 这样做的原因是为了在不同的 goroutine 中使用同一个上下文,同时避免一个 goroutine 中的操作影响到其他 goroutine 中的操作
15.DefaultWriter
如何记录日志
func main() {
gin.DisableConsoleColor()
f, _ := os.Create("gin.log")
// 指定输出流
gin.DefaultWriter = io.MultiWriter(f)
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
r.Run(":8080")
}
16.DebugPrintRouteFunc
定义路由日志的格式
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
r := gin.Default()
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
r.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
r.Run()
}
// 默认日志
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
// 自定义日志
2024/01/30 10:32:11 endpoint GET /status main.main.func2 3
17.ShouldBind ShouldBindBodyWith
将 request body 绑定到不同的结构体中
c.ShouldBind()
或 c.ShouldBindJSON()
方法将请求体绑定到不同的结构体中,这些方法会根据请求的 Content-Type 自动选择合适的解析器进行解析
绑定成功后,可以进行以下操作:
- 数据验证:检查绑定的数据是否符合预期的格式、类型和范围。例如,确保字符串长度符合要求,数字在有效范围内,或者某些字段是必填的。
- 业务逻辑处理:根据绑定的数据执行相应的业务逻辑。这可能包括数据库操作(如创建、更新或删除记录)、调用其他服务或执行计算等。
- 数据转换:在某些情况下,你可能需要对绑定的数据进行转换,以便它可以被后续的代码或库更好地使用。
- 返回响应:根据处理结果构造并返回适当的HTTP响应。这可能是一个JSON对象、XML文档、HTML页面或其他类型的内容。
- 错误处理:如果在验证、处理或转换数据时发生错误,你需要捕获这些错误并返回适当的错误响应。
- 日志记录:为了调试和监控目的,你可能想要记录请求的数据和处理的结果。
- 安全性检查:确保数据处理过程中遵循最佳安全实践,例如防止 SQL 注入、跨站脚本(XSS)攻击等。
- 授权和认证:如果应用程序需要用户登录或权限检查,你可能需要在处理请求之前验证用户的凭证。
- 缓存:对于重复的请求,你可能想要实现缓存机制以提高性能。
- 限流:为了防止滥用或过高的负载,你可能需要对 API 的访问频率进行限制。
绑定成功后的操作取决于你的具体需求和应用程序的业务逻辑。通常,你会将绑定的数据传递给后续的处理流程,最终生成一个响应返回给客户端。
使用 c.ShouldBindBodyWith 绑定,不可重用
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
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.String(200, "not objA or objB")
}
}
func main() {
r := gin.Default()
r.POST("/someHandler", SomeHandler)
r.Run(":8080")
}
使用 c.ShouldBindBodyWith 实现多次绑定
// 将 c.ShouldBind() 替换为 c.ShouldBindBodyWith()
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.String(200, "not objA or objB")
}
这时候,就可以根据传入的信息进行多个结构体的绑定
传入 {"foo": "v-foo"}
则响应 the body should be formA JSON
传入 {"bar": "v-bar"}
则响应 the body should be formB JSON
18.控制日志输出颜色
日志颜色输出是默认打开的
// 强制输出颜色
gin.ForceConsoleColor()
// 关闭颜色输出
gin.DisableConsoleColor()
19.QueryMap PostFormMap
映射查询字符串或表单参数
映射查询字符串或表单参数,通过 c.QueryMap
获取查询字符串,通过 c.PostFormMap
获取表单字符串
func main() {
r := gin.Default()
r.POST("/post", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")
fmt.Printf("ids=%v; names=%v", ids, names)
})
r.Run(":8080")
}
// 日志打印
ids=map[a:1234 b:hello]; names=map[a:1234 b:hello][GIN] 2024/01/30 - 15:12:08 | 200 | 636.5µs | ::1 | POST "/post?ids[a]=1234&ids[b]=hello"
20.Query
查询字符串参数
查询字符串参数,然后在应用中使用 url 传入的参数
r.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // // c.Request.URL.Query().Get("lastname") 的一种快捷方式
c.String(200, "hello %s %s", firstname, lastname)
})
// 测试访问
http://localhost:8080/welcome?lastname=xiaoyang
hello Guest xiaoyang
21.ShouldBindJSON
模型绑定和验证
Gin目前支持JSON、XML、YAML和标准表单值的绑定(foo=bar&boo=baz)
Gin提供了两类绑定方法:
-
Type - Must bind
-
Type - Should bind
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `"password" json:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
// 绑定 JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 验证登录信息
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// 绑定 XML (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>manu</user>
// <password>123</password>
// </root>)
router.POST("/loginXML", func(c *gin.Context) {
var xml Login
if err := c.ShouldBindXML(&xml); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if xml.User != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// 绑定 HTML 表单 (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// 根据 Content-Type Header 推断使用哪个绑定器。
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
router.Run(":8080")
}
// JSON 访问测试
{"manu": "123"} // JSON 数据
// 响应
{
"error": "Key: 'Login.User' Error:Field validation for 'User' failed on the 'required' tag\nKey: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"
}
{"user": "manu", "password": "123"} // JSON 数据
// 响应
{
"status": "you are logged in"
}
===================================
// XML 测试
(
<?xml version="1.0" encoding="UTF-8"?>
<root>
<user>manu</user>
<password>123</password>
</root>
)
// 响应
{
"status": "you are logged in"
}
22.Bind
绑定 HTML 复选框
将结构体和html中的 form 数据绑定在一起
package main
import (
"github.com/gin-gonic/gin"
)
type myForm struct {
Colors []string `form:"colors[]"`
}
func main() {
r := gin.Default()
r.LoadHTMLFiles("templates/form.html")
r.GET("/", indexHandler)
r.POST("/", formHandler)
r.Run(":8080")
}
func indexHandler(c *gin.Context) {
c.HTML(200, "form.html", nil)
}
func formHandler(c *gin.Context) {
var fakeForm myForm
// 获取 form.html 中选择的 color
c.Bind(&fakeForm)
c.JSON(200, gin.H{"color": fakeForm.Colors})
}
// 响应数据
{
"color": [
"red",
"green"
]
}
23.ShouldBindUri
绑定 Uri
在Gin框架中,当使用uri:"id" binding:"required,uuid"
这样的标签时,意味着该字段是一个必需的参数,并且它必须遵循UUID(通用唯一识别码)的标准格式。UUID是一种128位的数字,通常由32个十六进制数字组成,分为五组,形式为8-4-4-4-12,例如:123e4567-e89b-12d3-a456-426614174000
。这种格式确保了生成的ID是唯一且不重复的,这在创建数据库记录或进行资源定位时非常重要
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() {
r := gin.Default()
r.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()})
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
r.Run(":8080")
}
// 测试访问
http://localhost:8080/xiaoxie/1234
{"msg":"Key: 'Person.ID' Error:Field validation for 'ID' failed on the 'uuid' tag"}{"name":"xiaoxie","uuid":"1234"}
http://localhost:8080/xiaoxie/987fbc97-4bed-5078-9f07-9141ba07c9f3
{
"name": "xiaoxie",
"uuid": "987fbc97-4bed-5078-9f07-9141ba07c9f3"
}
24.ShouldBind
绑定查询字符串或表单数据
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}
func main() {
route := gin.Default()
route.POST("/testing", startPage)
route.Run(":8080")
}
func startPage(c *gin.Context) {
var person Person
if err := c.ShouldBindJSON(&person); err != nil {
log.Println("Error:", err)
} else {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
c.String(200, "Success1")
}
c.String(200, "Success2")
}
// 访问测试
http://localhost:8080/testing?name=xiaoxie&address=qinglu&birthday=2000-01-01
2024/01/31 00:02:54 xiaoxie
2024/01/31 00:02:54 qinglu
2024/01/31 00:02:54 2000-01-01 00:00:00 +0000 UTC
25.Bind
绑定表单数据至自定义结构体
package main
import "github.com/gin-gonic/gin"
type StructA struct {
FieldA string `form:"field_a"`
}
type StructB struct {
NestedStruct StructA
FieldB string `form:"field_b"`
}
type StructC struct {
NestedStructPointer *StructA
FieldC string `form:"field_c"`
}
type StructD struct {
NestedAnonyStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}
func GetDataB(c *gin.Context) {
var b StructB
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStruct,
"b": b.FieldB,
})
}
func GetDataC(c *gin.Context) {
var b StructC
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStructPointer,
"c": b.FieldC,
})
}
func GetDataD(c *gin.Context) {
var b StructD
c.Bind(&b)
c.JSON(200, gin.H{
"x": b.NestedAnonyStruct,
"d": b.FieldD,
})
}
func main() {
r := gin.Default()
r.GET("/getb", GetDataB)
r.GET("/getc", GetDataC)
r.GET("/getd", GetDataD)
r.Run()
}
// 访问测试
http://localhost:8080/getb?field_a=abc&field_b=bcd
{
"a": {
"FieldA": "abc"
},
"b": "bcd"
}
http://localhost:8080/getb?field_a=abc&field_c=bcd
{
"a": {
"FieldA": "abc"
},
"b": ""
}
目前仅支持没有 form 的嵌套结构体
26. ListenAndServe
自定义 HTTP 配置
在gin框架中可以直接使用 http.ListenAndServe()
来实现,用于启动一个服务器并监听指定的端口。它的作用是接受客户端的连接请求,并处理这些请求,然后返回相应的响应
ListenAndServe
:是Go语言标准库net/http
包中的一个函数,它用于启动一个HTTP服务器并监听指定的地址和端口。这个函数接受两个参数:一个是服务器要监听的地址,另一个是处理请求的handler接口实现。如果没有提供handler,它会使用默认的http.DefaultServeMux
来处理路由。r.Run()
:是Gin框架中用于启动服务器的方法。在Gin框架中,r.Run()
内部调用了ListenAndServe
函数,但它传入的是Gin框架自己的路由引擎Engine
作为handler。这意味着r.Run()
使用的是Gin框架提供的路由处理机制,而不是net/http
包的默认处理机制。
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
s := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
27. Use
自定义中间件
在Gin框架中,gin.New()
和gin.Default()
都用于初始化一个Engine实例,但它们之间存在一些差异。具体如下
gin.New()
:这个方法创建了一个新的Engine实例,但没有添加任何中间件。这意味着你需要手动添加所需的中间件,如日志记录和错误恢复等。gin.Default()
:这个方法不仅创建了一个新的Engine实例,还在其基础上添加了两个内置的中间件:Logger
和Recovery
。Logger中间件负责打印输出日志,方便开发者进行程序调试;Recovery
中间件的作用是在程序发生panic时能够捕获并恢复,防止程序崩溃。
总的来说,如果你需要一个完整的、带有日志和错误恢复功能的Gin引擎,可以直接使用gin.Default()
。如果你想要自定义这些功能或不需要这些内置的中间件,可以选择使用gin.New()
,然后根据需要添加相应的中间件
MustGet
和Get
在Gin框架中用于从请求的上下文中获取数据,但它们的行为有所不同
c.MustGet("key")
:这个方法会尝试从上下文中获取指定键的值。如果该键不存在,它会抛出一个panic。这意味着如果你确信请求中包含了这个键,你可以使用MustGet
来快速获取值,但如果键不存在,你需要处理可能的panic。c.Get("key")
:相比之下,Get
方法会返回两个值,第一个是请求上下文中键对应的值,第二个是一个布尔值,表示键是否存在。这种方法不会引发panic,即使键不存在,你也可以通过检查第二个返回值来判断是否成功获取了数据。
总的来说,如果你想要确保某个键在请求中必须存在,可以使用MustGet
,但你需要准备好处理可能的异常情况。如果你不确定键是否存在,或者希望在键不存在时采取某种默认行为,那么使用Get
会更加安全,因为它允许你根据键是否存在来做出决策
package main
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
func Logger() gin.HandleFunc {
return func(ctx *Context) {
t := time.Now()
ctx.Set("example", "1234")
ctx.Next()
latency := time.Since(t)
log.Printf("latency: %v\n", latency)
status := ctx.Writer.Status()
log.Printf("status: %v\n", status)
}
}
func main() {
r := gin.New()
r.Use(Logger())
r.GET("/test", func(ctx *Context) {
example := ctx.MustGet("example")
log.Println(example)
ctx.JSON(http.StatusOK, example)
})
r.Run(":8080")
}
// 有 ctx.Next()
2024/01/31 13:24:23 12345
2024/01/31 13:24:23 latency=1.0138ms
2024/01/31 13:24:23 status=200
// 无 ctx.Next()
2024/01/31 13:24:58 latency=0s
2024/01/31 13:24:58 status=200
2024/01/31 13:24:58 12345
具体来说,Next()
方法的使用可以分为以下几个关键点:
- 前置与后置拦截:在
Next()
之前的所有代码都会在到达具体的处理函数(HandlerFunc)之前执行,而Next()
之后的代码则会在处理函数执行完毕后执行。 - 数据传递:当前中间件调用
Next()
后,请求会继续传递到内层的中间件,这样可以实现跨中间件的数据传递和处理。 - 流程控制:如果中间件需要终止后续所有中间件的执行,可以调用
Abort()
方法,而不是Next()
。这通常用于处理错误或特定条件下的请求终止。
28.自定义验证器
binding.Validator.Engine().(*validator.Validate)
v.RegisterValidation("bookabledate", bookableDate)
自定义验证器注册到绑定引擎
package main
import (
"github.com/gin-gonic/gin"
"time"
"net/http"
)
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required, bookabledate" time_form:"2024-01-31"`
CheckOut time.Time `form:"check_out" binding:"required, gtfield=CheckIn, bookabledate" time_form:"2024-01-31"`
}
// 自定义验证器
var bookableDate validator.Func = func(f1 validator.FieldLevel) bool {
data, ok := f1.Field().Interface().(time.Time)
if ok {
today := time.Now()
if today.Year() > data.Year() || today.YearDay() > data.YearDay() {
return false
}
}
return true
}
func main() {
r := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
r.GET("/bookable", getBookable)
r.Run()
}
func getBookable(ctx *Context) {
var b Booking
if err := ctx.ShouldBindWith(&b, binding.Query); err == nil {
ctx.JSON(http.StatusOK, gin.H {
"msg": "Booking dates are valid"
})
} else {
ctx.JSON(http.StatusBadRequest, gin.H {
"error": err.Error()
})
}
}
// 访问测试
http://localhost:8080/bookable?check_in=2024-01-31&check_out=2024-02-01
{
"message": "Booking dates are valid!"
}
29.SetCookie
设置和获取 Cookie
Cookie是一种存储在用户本地终端上的数据,通常由服务器生成并通过HTTP响应头发送给用户浏览器。它主要用于在客户端和服务器之间保持状态信息。以下是Cookie的一些常见用途:
- 会话管理:Cookie可以用于识别用户并跟踪用户在网站上的活动。通过设置Session ID作为Cookie的值,服务器可以识别用户的请求并将其与特定的会话关联起来。
- 个性化体验:Cookie可以用于存储用户的偏好设置、语言选择等信息,从而提供个性化的网站体验。例如,当用户登录一个网站时,服务器可以将用户的信息存储在Cookie中,以便在后续的访问中快速识别用户并提供个性化的内容和服务。
- 购物车跟踪:在线购物网站可以使用Cookie来跟踪用户添加到购物车中的商品。即使用户关闭了浏览器或离开了网站,购物车中的商品仍然可以通过读取Cookie来恢复。
- 广告定位:第三方广告商可以使用Cookie来收集用户的兴趣和行为数据,并根据这些信息向用户展示相关的广告。这有助于提高广告的针对性和效果。
- 记住登录状态:许多网站使用Cookie来记住用户的登录状态,这样用户在下次访问时就不必再次输入用户名和密码。
需要注意的是,由于Cookie存储在用户的设备上,因此可能存在安全风险。为了保护用户的隐私和数据安全,应该采取适当的措施来加密和保护Cookie中的信息,并遵循相关的法律法规和最佳实践。
package main
import (
"github.com/gin-gonic/gin"
"fmt"
)
func main() {
r := gin.Default()
r.GET("/cookie", func(ctx *Context) {
cookie, err := ctx.Cookie("gin_cookie")
if err != nil {
cookie = "no-set"
ctx.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("cookie value: %s, %s\n", cookie, err)
})
r.Run()
}
Cookie value: NotSet, http: named cookie not present
[GIN] 2024/01/31 - 15:21:08 | 200 | 729.2µs | ::1 | GET "/cookie"
Cookie value: test, %!s(<nil>)
[GIN] 2024/01/31 - 15:22:32 | 200 | 0s | ::1 | GET "/cookie"
30.路由参数
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
// 此 handler 将匹配到 /user/xiaoxie但不会匹配到 /user 或者 /user/
r.GET("/user/:name", func(ctx *Content) {
name := ctx.Param("name")
ctx.String(http.StatusOK, name)
})
// 此 handler 将匹配到 /user/xiaoxie/haha 和 /user/xiaoxie
// 如果 /user/xiaoxie 没有其他的 handler,则就使用此 handler
r.GET("/user/:name/*action", func(ctx *Content) {
name := ctx.Param("name")
action := ctx.Param("action")
message := name + "is" + action
ctx.String(http.StatusOK, message)
})
r.Run()
}
// 访问测试
31.gin.Group
路由组
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func loginEndpoint(c *gin.Context) {
c.String(200, c.Request.URL.String())
}
func submitEndpoint(c *gin.Context) {
c.String(200, c.Request.URL.String())
}
func readEndpoint(c *gin.Context) {
c.String(200, c.Request.URL.String())
}
func main() {
r := gin.Default()
v1 := gin.Group("/v1") {
v1.GET("/login", func(c *Context) {c.String(http.StatusOK, c.Request.Url.String())})
v1.GET("/submit", func(c *Context) {c.String(http.StatusOK, c.Request.Url.String())})
v1.GET("/read", func(c *Context) {c.String(http.StatusOK, c.Request.Url.String())})
}
v2 := gin.Group("/v2") {
v2.GET("/login", func(c *Context) {c.String(http.StatusOK, c.Request.Url.String())})
v2.GET("/submit", func(c *Context) {c.String(http.StatusOK, c.Request.Url.String())})
v2.GET("/read", func(c *Context) {c.String(http.StatusOK, c.Request.Url.String())})
}
r.Run()
}
32.运行多个服务
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"time"
)
var (
g errgroup.Group
)
func router01() http.Handle {
e := gin.New()
e.User(gin.Recovery())
e.GET("/", func(ctx *Context) {
ctx.JSON(
http.StatusOK,
gin.H {
"code": http.StatusOK,
"msg": "Welcome server01"
}
)
})
}
func router02() http.Handle {
e := gin.New()
e.User(gin.Recovery())
e.GET("/", func(ctx *Context) {
ctx.JSON(
http.StatusOK,
gin.H {
"code": http.StatusOK,
"msg": "Welcome server02"
}
)
})
}
func main() {
server01 := &http.Server {
Addr: "8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server {
Addr: "8081",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
g.Go(func() error {
return server01.ListenAndServer
})
g.Go(func() error {
return server02.ListenAndServer
})
if err != nil {
log.Fatal(err)
}
}
// 访问测试
http://localhost:8080/
{
"code": 200,
"msg": "Welcome server 01"
}
http://localhost:8081/
{
"code": 200,
"msg": "Welcome server 02"
}
33.重定向
gin框架中的重定向, HTTP 重定向很容易。 内部、外部重定向均支持
package main
import (
"github/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/test", func(ctx *Context) {
ctx.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
})
r.POST("/test", func(ctx *Context) {
ctx.Redirect(http.StatusFound, "/route2")
})
r.GET("/route", func(ctx *Context) {
ctx.Request.Url.Path = "/route2"
r.HandleContext(c)
})
r.GET("/route2", func(ctx *Context) {
c.JSON(http.StatusOK, "hello, world")
})
r.Run()
}
静态文件服务
router.Static
:- 用途:用于将特定目录下的所有文件作为静态文件提供服务。
- 配置:接受两个参数,第一个参数是URL的前缀(例如
/assets
),第二个参数是对应的本地文件系统路径(例如./assets
)。 - 特点:访问这个URL前缀下的任何文件,Gin框架会尝试在指定的本地目录中找到并返回相应的文件。
router.StaticFS
:- 用途:用于将一个文件系统对象(如目录或文件)作为静态文件提供服务。
- 配置:接受两个参数,第一个参数是URL的前缀(例如
/more_static
),第二个参数是一个http.FileSystem
对象,通常是通过http.Dir
函数创建的目录对象。 - 特点:与
router.Static
类似,但是更加灵活,因为它可以接受任何实现了http.FileSystem
接口的对象,不仅限于本地目录。
router.StaticFile
:- 用途:用于将单个文件作为静态文件提供服务。
- 配置:接受两个参数,第一个参数是URL的路径(例如
/user.jpg
),第二个参数是对应文件的本地路径(例如./resources/user.jpg
)。 - 特点:这种方式是为单个文件提供服务,而不是为整个目录。当访问指定的URL时,Gin框架会返回对应的单个文件。
总结来说,router.Static
用于服务整个目录的静态文件,router.StaticFS
用于服务文件系统对象(可以是目录或其他实现了http.FileSystem
接口的对象),而router.StaticFile
用于服务单个文件。在性能方面,router.StaticFile
对于单个文件的服务通常更快,因为它不需要遍历目录来查找文件。
func main() {
router := gin.Default()
router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("/var/log")) //gin.Dir("/var/log", true)
router.StaticFile("/user.jpg", "./resources/user.jpg")
// 监听并在 0.0.0.0:8080 上启动服务
router.Run(":8080")
}