记录Gin官方文档学习过程

Gin 介绍

概述

Gin 是一个使用 go 语言编写的 web 框架,是一个高性能,简洁的框架

文档 | Gin Web Framework (gin-gonic.com)

前景 · Go语言中文文档 (topgoer.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)
	})

测试结果

http://localhost:8080/

{"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.Querypost form

Query 通常与 HTTP GET 请求一起使用,而 Post Form 通常与 HTTP POST 请求一起使用,用于在 Web 开发中进行数据传递和提交

在Web开发中,"Query"和"Post Form"是两种不同的方式来发送数据到服务器的。它们的区别主要在于数据传输的方式和用途:

  1. 查询(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")
        // 使用获取到的参数进行处理
    }
    
  2. 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")
}

image-20240130093830117


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 自动选择合适的解析器进行解析

绑定成功后,可以进行以下操作:

  1. 数据验证:检查绑定的数据是否符合预期的格式、类型和范围。例如,确保字符串长度符合要求,数字在有效范围内,或者某些字段是必填的。
  2. 业务逻辑处理:根据绑定的数据执行相应的业务逻辑。这可能包括数据库操作(如创建、更新或删除记录)、调用其他服务或执行计算等。
  3. 数据转换:在某些情况下,你可能需要对绑定的数据进行转换,以便它可以被后续的代码或库更好地使用。
  4. 返回响应:根据处理结果构造并返回适当的HTTP响应。这可能是一个JSON对象、XML文档、HTML页面或其他类型的内容。
  5. 错误处理:如果在验证、处理或转换数据时发生错误,你需要捕获这些错误并返回适当的错误响应。
  6. 日志记录:为了调试和监控目的,你可能想要记录请求的数据和处理的结果。
  7. 安全性检查:确保数据处理过程中遵循最佳安全实践,例如防止 SQL 注入、跨站脚本(XSS)攻击等。
  8. 授权和认证:如果应用程序需要用户登录或权限检查,你可能需要在处理请求之前验证用户的凭证。
  9. 缓存:对于重复的请求,你可能想要实现缓存机制以提高性能。
  10. 限流:为了防止滥用或过高的负载,你可能需要对 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")
}

image-20240130130910146

image-20240130131142317

使用 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()

image-20240130140823464

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")
}

image-20240130151300238

// 日志打印
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})
}

image-20240130184140161


// 响应数据
{
    "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实例,还在其基础上添加了两个内置的中间件:LoggerRecovery。Logger中间件负责打印输出日志,方便开发者进行程序调试;Recovery 中间件的作用是在程序发生panic时能够捕获并恢复,防止程序崩溃。

总的来说,如果你需要一个完整的、带有日志和错误恢复功能的Gin引擎,可以直接使用gin.Default()。如果你想要自定义这些功能或不需要这些内置的中间件,可以选择使用gin.New(),然后根据需要添加相应的中间件

MustGetGet在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!"
}

Cookie是一种存储在用户本地终端上的数据,通常由服务器生成并通过HTTP响应头发送给用户浏览器。它主要用于在客户端和服务器之间保持状态信息。以下是Cookie的一些常见用途:

  1. 会话管理:Cookie可以用于识别用户并跟踪用户在网站上的活动。通过设置Session ID作为Cookie的值,服务器可以识别用户的请求并将其与特定的会话关联起来。
  2. 个性化体验:Cookie可以用于存储用户的偏好设置、语言选择等信息,从而提供个性化的网站体验。例如,当用户登录一个网站时,服务器可以将用户的信息存储在Cookie中,以便在后续的访问中快速识别用户并提供个性化的内容和服务。
  3. 购物车跟踪:在线购物网站可以使用Cookie来跟踪用户添加到购物车中的商品。即使用户关闭了浏览器或离开了网站,购物车中的商品仍然可以通过读取Cookie来恢复。
  4. 广告定位:第三方广告商可以使用Cookie来收集用户的兴趣和行为数据,并根据这些信息向用户展示相关的广告。这有助于提高广告的针对性和效果。
  5. 记住登录状态:许多网站使用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()
}

静态文件服务

  1. router.Static:
    • 用途:用于将特定目录下的所有文件作为静态文件提供服务。
    • 配置:接受两个参数,第一个参数是URL的前缀(例如/assets),第二个参数是对应的本地文件系统路径(例如./assets)。
    • 特点:访问这个URL前缀下的任何文件,Gin框架会尝试在指定的本地目录中找到并返回相应的文件。
  2. router.StaticFS:
    • 用途:用于将一个文件系统对象(如目录或文件)作为静态文件提供服务。
    • 配置:接受两个参数,第一个参数是URL的前缀(例如/more_static),第二个参数是一个http.FileSystem对象,通常是通过http.Dir函数创建的目录对象。
    • 特点:与router.Static类似,但是更加灵活,因为它可以接受任何实现了http.FileSystem接口的对象,不仅限于本地目录。
  3. 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")
}

学习 curl 如何使用

posted @ 2025-03-28 18:54  小依昂阳  阅读(59)  评论(0)    收藏  举报