1375 lines
35 KiB
Markdown
1375 lines
35 KiB
Markdown
# Gin框架基础
|
||
|
||
# Gin框架简介
|
||
|
||
Gin是一个用Go语言编写的Web框架,它基于httprouter,提供了一种简单、快速、轻量级的方式来构建Web应用和API服务。Gin框架以其高性能和低内存占用而闻名,是目前Go语言中最受欢迎的Web框架之一。
|
||
|
||
## 1.1 Gin框架的特点
|
||
|
||
- **高性能**:基于Radix树路由匹配算法,速度极快
|
||
- **轻量级**:核心库代码量少,依赖少
|
||
- **中间件支持**:提供丰富的中间件机制
|
||
- **优雅的API**:简洁易用的接口设计
|
||
- **JSON验证**:能够方便地验证和解析JSON请求
|
||
- **路由分组**:支持路由分组,便于管理API版本和权限控制
|
||
- **错误管理**:内置错误处理机制
|
||
- **内置渲染**:支持JSON、XML、HTML等多种响应格式
|
||
|
||
## 1.2 与其他Go Web框架的比较
|
||
|
||
|框架|特点|适用场景|
|
||
|---|---|---|
|
||
|Gin|高性能、轻量级、简洁API|高性能API服务、微服务|
|
||
|Echo|高性能、可扩展、极简主义|全栈Web应用|
|
||
|Fiber|类Express设计、基于fasthttp|高性能Web服务|
|
||
|Gorilla|模块化设计、灵活性高|复杂Web应用|
|
||
|
||
# 第一个Gin应用
|
||
|
||
## 3.1 Hello World示例
|
||
|
||
创建`main.go`文件:
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
package main
|
||
|
||
import "github.com/gin-gonic/gin"
|
||
|
||
func main() {
|
||
// 创建默认的路由引擎
|
||
r := gin.Default()
|
||
|
||
// 定义一个GET请求处理
|
||
r.GET("/", func(c *gin.Context) {
|
||
c.String(200, "Hello, Gin!")
|
||
})
|
||
|
||
// 启动服务器
|
||
r.Run(":8080") // 监听并在0.0.0.0:8080上启动服务
|
||
}
|
||
```
|
||
|
||
运行应用:
|
||
|
||
bash复制代码
|
||
|
||
```bash
|
||
go run main.go
|
||
```
|
||
|
||
访问`http://localhost:8080`即可看到"Hello, Gin!"
|
||
|
||
# 路由(Route)
|
||
|
||
Gin提供了强大而灵活的路由机制,支持各种HTTP方法和路径匹配。
|
||
|
||
## 4.1 基本路由
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// GET方法
|
||
r.GET("/hello", func(c *gin.Context) {
|
||
c.String(200, "Hello GET")
|
||
})
|
||
|
||
// POST方法
|
||
r.POST("/submit", func(c *gin.Context) {
|
||
c.String(200, "Hello POST")
|
||
})
|
||
|
||
// PUT方法
|
||
r.PUT("/update", func(c *gin.Context) {
|
||
c.String(200, "Hello PUT")
|
||
})
|
||
|
||
// DELETE方法
|
||
r.DELETE("/remove", func(c *gin.Context) {
|
||
c.String(200, "Hello DELETE")
|
||
})
|
||
|
||
// 匹配所有HTTP方法
|
||
r.Any("/anything", func(c *gin.Context) {
|
||
c.String(200, "Hello Any Method")
|
||
})
|
||
```
|
||
|
||
## 4.2 路径参数
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 获取路径参数
|
||
r.GET("/user/:name", func(c *gin.Context) {
|
||
name := c.Param("name")
|
||
c.String(200, "Hello %s", name)
|
||
})
|
||
|
||
// 使用*通配符
|
||
r.GET("/file/*filepath", func(c *gin.Context) {
|
||
filepath := c.Param("filepath")
|
||
c.String(200, "File path: %s", filepath)
|
||
})
|
||
```
|
||
|
||
## 4.3 查询参数
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 获取查询参数
|
||
r.GET("/search", func(c *gin.Context) {
|
||
query := c.Query("q") // 获取参数,没有则返回空字符串
|
||
page := c.DefaultQuery("page", "1") // 获取参数,没有则使用默认值
|
||
|
||
c.JSON(200, gin.H{
|
||
"query": query,
|
||
"page": page,
|
||
})
|
||
})
|
||
```
|
||
|
||
## 4.4 路由分组
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 创建v1版本的API组
|
||
v1 := r.Group("/v1")
|
||
{
|
||
v1.GET("/users", getUsers)
|
||
v1.POST("/users", createUser)
|
||
v1.GET("/users/:id", getUserByID)
|
||
}
|
||
|
||
// 创建v2版本的API组
|
||
v2 := r.Group("/v2")
|
||
{
|
||
v2.GET("/users", getUsersV2)
|
||
v2.POST("/users", createUserV2)
|
||
}
|
||
```
|
||
|
||
# 中间件(Middleware)
|
||
|
||
中间件是Gin框架的核心特性之一,允许在请求处理过程中执行代码、修改请求和响应,以及终止请求处理流程。
|
||
|
||
## 5.1 内置中间件
|
||
|
||
Gin提供了多种内置中间件:
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 默认使用Logger和Recovery中间件
|
||
r := gin.Default()
|
||
|
||
// 不使用任何中间件
|
||
r := gin.New()
|
||
|
||
// 手动添加Logger中间件
|
||
r.Use(gin.Logger())
|
||
|
||
// 手动添加Recovery中间件(从任何panic恢复)
|
||
r.Use(gin.Recovery())
|
||
```
|
||
|
||
## 5.2 自定义中间件
|
||
|
||
创建自定义中间件:
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
func AuthMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 中间件逻辑(请求前)
|
||
token := c.GetHeader("Authorization")
|
||
if token != "valid-token" {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
||
return
|
||
}
|
||
|
||
// 处理请求
|
||
c.Next()
|
||
|
||
// 中间件逻辑(请求后)
|
||
// 可以访问响应状态等
|
||
}
|
||
}
|
||
|
||
// 全局使用中间件
|
||
r.Use(AuthMiddleware())
|
||
|
||
// 对特定路由组使用中间件
|
||
authorized := r.Group("/admin")
|
||
authorized.Use(AuthMiddleware())
|
||
{
|
||
authorized.GET("/secrets", getSecrets)
|
||
}
|
||
|
||
// 对单个路由使用中间件
|
||
r.GET("/sensitive", AuthMiddleware(), getSensitiveData)
|
||
```
|
||
|
||
## 5.3 中间件间数据传递
|
||
|
||
在Gin中,中间件之间或中间件与处理函数之间可以通过Context上下文共享数据。Gin提供了一系列方法来设置和获取Context中的值:
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
func DataMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 在中间件中设置值
|
||
c.Set("userId", 1001)
|
||
c.Set("username", "张三")
|
||
|
||
// 继续处理请求
|
||
c.Next()
|
||
|
||
// 请求完成后的处理
|
||
// 可以在这里记录响应时间等信息
|
||
}
|
||
}
|
||
|
||
func AuthRequired() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 从上一个中间件获取值
|
||
userId, exists := c.Get("userId")
|
||
if !exists {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "未授权访问"})
|
||
return
|
||
}
|
||
|
||
// 可以根据userId进行进一步的权限检查
|
||
c.Set("role", "admin") // 设置新的值供后续中间件或处理函数使用
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// 在路由处理函数中使用
|
||
r.GET("/user/profile", DataMiddleware(), AuthRequired(), func(c *gin.Context) {
|
||
// 获取中间件设置的值
|
||
userId := c.GetInt("userId")
|
||
username := c.GetString("username")
|
||
role := c.GetString("role")
|
||
|
||
// 获取特定类型的值,并进行类型断言
|
||
userIdInt, ok := userId.(int)
|
||
if !ok {
|
||
c.JSON(500, gin.H{"error": "类型断言失败"})
|
||
return
|
||
}
|
||
|
||
// 使用MustGet,如果值不存在会panic
|
||
// 在确定值一定存在的情况下使用
|
||
role := c.MustGet("role").(string)
|
||
|
||
c.JSON(200, gin.H{
|
||
"userId": userIdInt,
|
||
"username": username,
|
||
"role": role,
|
||
})
|
||
})
|
||
```
|
||
|
||
## 5.3.1 主要方法说明
|
||
|
||
1. **c.Set(key string, value interface{})**
|
||
|
||
- 设置键值对到当前请求的上下文中
|
||
- 可以在中间件或处理函数中使用
|
||
- 设置的值仅在当前请求周期内有效
|
||
2. **c.Get(key string) (value interface{}, exists bool)**
|
||
|
||
- 从当前请求的上下文中获取值
|
||
- 返回值和是否存在的标志
|
||
- 需要进行类型断言才能使用具体类型的值
|
||
3. **c.MustGet(key string) interface{}**
|
||
|
||
- 从当前请求的上下文中获取值
|
||
- 如果值不存在,会触发panic
|
||
- 仅在确定值一定存在的情况下使用
|
||
4. **c.GetString(key string) (s string)**
|
||
|
||
- 从上下文中获取字符串值
|
||
- 如果值不存在或不是字符串类型,返回空字符串
|
||
5. **c.GetBool(key string) (b bool)**
|
||
|
||
- 从上下文中获取布尔值
|
||
- 如果值不存在或不是布尔类型,返回false
|
||
6. **c.GetInt(key string) (i int)**
|
||
|
||
- 从上下文中获取整数值
|
||
- 如果值不存在或不是整数类型,返回0
|
||
7. **c.GetInt64(key string) (i64 int64)**
|
||
|
||
- 从上下文中获取64位整数值
|
||
- 如果值不存在或不是64位整数类型,返回0
|
||
8. **c.GetFloat64(key string) (f64 float64)**
|
||
|
||
- 从上下文中获取64位浮点数值
|
||
- 如果值不存在或不是64位浮点数类型,返回0
|
||
9. **c.GetTime(key string) (t time.Time)**
|
||
|
||
- 从上下文中获取时间值
|
||
- 如果值不存在或不是时间类型,返回零值时间
|
||
10. **c.GetDuration(key string) (d time.Duration)**
|
||
|
||
- 从上下文中获取时间间隔值
|
||
- 如果值不存在或不是时间间隔类型,返回0
|
||
11. **c.GetStringSlice(key string) (ss []string)**
|
||
|
||
- 从上下文中获取字符串切片
|
||
- 如果值不存在或不是字符串切片类型,返回nil
|
||
12. **c.GetStringMap(key string) (sm map[string]interface{})**
|
||
|
||
- 从上下文中获取字符串映射
|
||
- 如果值不存在或不是字符串映射类型,返回nil
|
||
13. **c.GetStringMapString(key string) (sms map[string]string)**
|
||
|
||
- 从上下文中获取字符串到字符串的映射
|
||
- 如果值不存在或不是字符串到字符串的映射类型,返回nil
|
||
|
||
## 5.3.2 实际应用场景
|
||
|
||
1. **用户认证**:在认证中间件中验证用户身份,然后将用户ID和角色设置到上下文中,供后续处理函数使用。
|
||
|
||
2. **日志记录**:在请求开始时记录时间戳,在请求结束时计算处理时间并记录日志。
|
||
|
||
3. **错误处理**:在中间件中捕获处理函数中的错误,并统一处理。
|
||
|
||
4. **权限控制**:基于用户角色进行访问控制。
|
||
|
||
|
||
# 六、请求数据处理
|
||
|
||
## 6.1 表单数据
|
||
|
||
```go
|
||
r.POST("/form", func(c *gin.Context) {
|
||
// 获取表单参数
|
||
username := c.PostForm("username")
|
||
password := c.DefaultPostForm("password", "default_password")
|
||
|
||
c.JSON(200, gin.H{
|
||
"username": username,
|
||
"password": password,
|
||
})
|
||
})
|
||
```
|
||
|
||
|
||
## 6.2 JSON数据
|
||
|
||
```go
|
||
type LoginForm struct {
|
||
Username string `json:"username" binding:"required"`
|
||
Password string `json:"password" binding:"required"`
|
||
}
|
||
|
||
r.POST("/login", func(c *gin.Context) {
|
||
var form LoginForm
|
||
|
||
// 绑定JSON数据到结构体
|
||
if err := c.ShouldBindJSON(&form); err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 处理登录逻辑...
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "登录成功",
|
||
"username": form.Username,
|
||
})
|
||
})
|
||
```
|
||
|
||
## 6.3 文件上传
|
||
|
||
```go
|
||
r.POST("/upload", func(c *gin.Context) {
|
||
// 单文件上传
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 保存文件
|
||
dst := "./uploads/" + file.Filename
|
||
if err := c.SaveUploadedFile(file, dst); err != nil {
|
||
c.JSON(500, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "文件上传成功",
|
||
"filename": file.Filename,
|
||
})
|
||
})
|
||
|
||
r.POST("/multi-upload", func(c *gin.Context) {
|
||
// 多文件上传
|
||
form, err := c.MultipartForm()
|
||
if err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
files := form.File["files"]
|
||
filenames := []string{}
|
||
|
||
for _, file := range files {
|
||
dst := "./uploads/" + file.Filename
|
||
if err := c.SaveUploadedFile(file, dst); err != nil {
|
||
c.JSON(500, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
filenames = append(filenames, file.Filename)
|
||
}
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "多文件上传成功",
|
||
"filenames": filenames,
|
||
})
|
||
})
|
||
```
|
||
|
||
# 七、响应数据处理
|
||
|
||
## 7.1 JSON响应
|
||
|
||
|
||
```go
|
||
r.GET("/json", func(c *gin.Context) {
|
||
c.JSON(200, gin.H{
|
||
"message": "这是一个JSON响应",
|
||
"status": "success",
|
||
"data": gin.H{
|
||
"name": "张三",
|
||
"age": 25,
|
||
},
|
||
})
|
||
})
|
||
```
|
||
|
||
## 7.2 XML响应
|
||
|
||
|
||
```go
|
||
r.GET("/xml", func(c *gin.Context) {
|
||
c.XML(200, gin.H{
|
||
"message": "这是一个XML响应",
|
||
"status": "success",
|
||
})
|
||
})
|
||
```
|
||
|
||
## 7.3 HTML响应
|
||
|
||
首先需要加载模板:
|
||
|
||
|
||
```go
|
||
r.LoadHTMLGlob("templates/*")
|
||
|
||
r.GET("/html", func(c *gin.Context) {
|
||
c.HTML(200, "index.tmpl", gin.H{
|
||
"title": "Gin框架",
|
||
"message": "欢迎学习Gin框架",
|
||
})
|
||
})
|
||
```
|
||
|
||
创建模板文件`templates/index.tmpl`:
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>{{.title}}</title>
|
||
</head>
|
||
<body>
|
||
<h1>{{.message}}</h1>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
## 7.4 文件下载
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
//下载文件
|
||
r.GET("/download", func(c *gin.Context) {
|
||
c.File("./files/sample.pdf")
|
||
})
|
||
|
||
// 下载文件并设置文件名
|
||
r.GET("/download-attachment", func(c *gin.Context) {
|
||
c.FileAttachment("./files/sample.pdf", "用户手册.pdf")
|
||
})
|
||
```
|
||
|
||
## 7.5 HTTP Header和Cookie设置
|
||
|
||
在Web开发中,设置HTTP响应头和Cookie是常见的需求。Gin提供了简单易用的方法来处理这些操作。
|
||
|
||
### 7.5.1 设置HTTP Header
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 设置单个响应头
|
||
r.GET("/header", func(c *gin.Context) {
|
||
c.Header("X-Custom-Header", "自定义头部值")
|
||
c.Header("Content-Type", "application/json; charset=utf-8")
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "成功设置自定义Header",
|
||
})
|
||
})
|
||
|
||
// 设置多个响应头
|
||
r.GET("/headers", func(c *gin.Context) {
|
||
// 设置CORS相关头部
|
||
c.Header("Access-Control-Allow-Origin", "*")
|
||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||
|
||
// 设置缓存控制
|
||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||
c.Header("Pragma", "no-cache")
|
||
c.Header("Expires", "0")
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "设置了多个HTTP头部",
|
||
})
|
||
})
|
||
|
||
// 设置安全相关头部
|
||
r.GET("/secure", func(c *gin.Context) {
|
||
c.Header("X-Content-Type-Options", "nosniff")
|
||
c.Header("X-Frame-Options", "DENY")
|
||
c.Header("X-XSS-Protection", "1; mode=block")
|
||
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "设置了安全相关的HTTP头部",
|
||
})
|
||
})
|
||
```
|
||
|
||
### 7.5.2 Cookie操作
|
||
|
||
|
||
```go
|
||
// 设置Cookie
|
||
r.GET("/set-cookie", func(c *gin.Context) {
|
||
// 基本Cookie设置
|
||
// SetCookie(name, value, maxAge, path, domain, secure, httpOnly)
|
||
c.SetCookie("username", "张三", 3600, "/", "localhost", false, true)
|
||
|
||
// 设置Session Cookie (浏览器关闭时过期)
|
||
c.SetCookie("session_id", "abc123def456", 0, "/", "", false, true)
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "Cookie已设置",
|
||
})
|
||
})
|
||
|
||
// 读取Cookie
|
||
r.GET("/get-cookie", func(c *gin.Context) {
|
||
username, err := c.Cookie("username")
|
||
if err != nil {
|
||
c.JSON(400, gin.H{
|
||
"error": "无法获取Cookie: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
sessionId, err := c.Cookie("session_id")
|
||
if err != nil {
|
||
sessionId = "未设置"
|
||
}
|
||
|
||
c.JSON(200, gin.H{
|
||
"username": username,
|
||
"session_id": sessionId,
|
||
})
|
||
})
|
||
|
||
// 删除Cookie
|
||
r.GET("/delete-cookie", func(c *gin.Context) {
|
||
// 通过设置过期时间为-1来删除Cookie
|
||
c.SetCookie("username", "", -1, "/", "localhost", false, true)
|
||
c.SetCookie("session_id", "", -1, "/", "", false, true)
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "Cookie已删除",
|
||
})
|
||
})
|
||
```
|
||
|
||
### 7.5.3 Cookie参数详解
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// Cookie参数详细说明
|
||
r.GET("/cookie-details", func(c *gin.Context) {
|
||
// SetCookie参数说明:
|
||
// name: Cookie名称
|
||
// value: Cookie值
|
||
// maxAge: 过期时间(秒),0表示会话Cookie,-1表示立即过期(删除Cookie)
|
||
// path: Cookie路径,"/"表示整个网站可用
|
||
// domain: Cookie域名,空字符串表示当前域名
|
||
// secure: 是否只在HTTPS连接中发送Cookie
|
||
// httpOnly: 是否禁止JavaScript访问Cookie(提高安全性)
|
||
|
||
// 示例:设置一个安全的登录Cookie
|
||
c.SetCookie(
|
||
"auth_token", // Cookie名称
|
||
"secure_token_value", // Cookie值
|
||
86400, // 过期时间:24小时 (24*60*60)
|
||
"/", // 路径:整个网站
|
||
"yourdomain.com", // 域名
|
||
true, // 仅HTTPS
|
||
true, // HttpOnly
|
||
)
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "已设置安全的认证Cookie",
|
||
})
|
||
})
|
||
```
|
||
|
||
### 7.5.4 实际应用场景
|
||
|
||
|
||
```go
|
||
// 用户登录设置Cookie
|
||
r.POST("/login", func(c *gin.Context) {
|
||
var loginData struct {
|
||
Username string `json:"username" binding:"required"`
|
||
Password string `json:"password" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&loginData); err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 这里应该验证用户名和密码
|
||
if loginData.Username == "admin" && loginData.Password == "password" {
|
||
// 登录成功,设置认证Cookie
|
||
c.SetCookie("user_id", "1001", 86400, "/", "", false, true)
|
||
c.SetCookie("username", loginData.Username, 86400, "/", "", false, false)
|
||
|
||
// 设置自定义响应头
|
||
c.Header("X-Login-Time", time.Now().Format(time.RFC3339))
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "登录成功",
|
||
"username": loginData.Username,
|
||
})
|
||
} else {
|
||
// 登录失败,设置错误响应头
|
||
c.Header("X-Login-Failed", "true")
|
||
c.JSON(401, gin.H{
|
||
"error": "用户名或密码错误",
|
||
})
|
||
}
|
||
})
|
||
|
||
// 用户注销清除Cookie
|
||
r.POST("/logout", func(c *gin.Context) {
|
||
// 删除认证相关的Cookie
|
||
c.SetCookie("user_id", "", -1, "/", "", false, true)
|
||
c.SetCookie("username", "", -1, "/", "", false, false)
|
||
|
||
// 设置注销时间响应头
|
||
c.Header("X-Logout-Time", time.Now().Format(time.RFC3339))
|
||
|
||
c.JSON(200, gin.H{
|
||
"message": "注销成功",
|
||
})
|
||
})
|
||
|
||
// 需要认证的路由
|
||
r.GET("/profile", func(c *gin.Context) {
|
||
// 检查认证Cookie
|
||
userID, err := c.Cookie("user_id")
|
||
if err != nil {
|
||
c.Header("X-Auth-Required", "true")
|
||
c.JSON(401, gin.H{
|
||
"error": "需要登录",
|
||
})
|
||
return
|
||
}
|
||
|
||
username, _ := c.Cookie("username")
|
||
|
||
c.JSON(200, gin.H{
|
||
"user_id": userID,
|
||
"username": username,
|
||
"message": "用户信息获取成功",
|
||
})
|
||
})
|
||
```
|
||
|
||
### 7.5.5 中间件中设置Header和Cookie
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 创建设置通用响应头的中间件
|
||
func CommonHeadersMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 设置API版本
|
||
c.Header("X-API-Version", "v1.0.0")
|
||
|
||
// 设置服务器信息
|
||
c.Header("X-Powered-By", "Gin Framework")
|
||
|
||
// 设置响应时间
|
||
c.Header("X-Response-Time", time.Now().Format(time.RFC3339))
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// 创建CORS中间件
|
||
func CORSMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
c.Header("Access-Control-Allow-Origin", "*")
|
||
c.Header("Access-Control-Allow-Credentials", "true")
|
||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||
|
||
if c.Request.Method == "OPTIONS" {
|
||
c.AbortWithStatus(204)
|
||
return
|
||
}
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// 在路由中使用中间件
|
||
func setupRouter() *gin.Engine {
|
||
r := gin.Default()
|
||
|
||
// 全局应用中间件
|
||
r.Use(CommonHeadersMiddleware())
|
||
r.Use(CORSMiddleware())
|
||
|
||
// 定义路由...
|
||
|
||
return r
|
||
}
|
||
```
|
||
|
||
# 八、模板渲染
|
||
|
||
## 8.1 基本用法
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 加载模板
|
||
r.LoadHTMLGlob("templates/*")
|
||
// 或者
|
||
r.LoadHTMLFiles("templates/index.tmpl", "templates/user.tmpl")
|
||
|
||
r.GET("/", func(c *gin.Context) {
|
||
c.HTML(200, "index.tmpl", gin.H{
|
||
"title": "Gin框架模板示例",
|
||
})
|
||
})
|
||
```
|
||
|
||
## 8.2 模板语法
|
||
|
||
模板文件`templates/users.tmpl`:
|
||
|
||
html复制代码
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>{{.title}}</title>
|
||
</head>
|
||
<body>
|
||
<h1>用户列表</h1>
|
||
<ul>
|
||
{{range .users}}
|
||
<li>{{.Name}} - {{.Age}}岁</li>
|
||
{{end}}
|
||
</ul>
|
||
|
||
{{if .showAdmins}}
|
||
<h2>管理员</h2>
|
||
<ul>
|
||
{{range .admins}}
|
||
<li>{{.}}</li>
|
||
{{end}}
|
||
</ul>
|
||
{{else}}
|
||
<p>没有显示管理员信息</p>
|
||
{{end}}
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
对应的处理函数:
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
r.GET("/users", func(c *gin.Context) {
|
||
users := []struct{
|
||
Name string
|
||
Age int
|
||
}{
|
||
{"张三", 25},
|
||
{"李四", 30},
|
||
{"王五", 28},
|
||
}
|
||
|
||
c.HTML(200, "users.tmpl", gin.H{
|
||
"title": "用户列表",
|
||
"users": users,
|
||
"showAdmins": true,
|
||
"admins": []string{"admin1", "admin2"},
|
||
})
|
||
})
|
||
```
|
||
|
||
## 8.3 静态文件服务
|
||
|
||
提供静态文件(CSS、JS、图片等):
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 单个静态目录
|
||
r.Static("/assets", "./static")
|
||
|
||
// 单个文件
|
||
r.StaticFile("/favicon.ico", "./resources/favicon.ico")
|
||
|
||
// 多个静态目录
|
||
r.StaticFS("/more_static", http.Dir("./public"))
|
||
```
|
||
|
||
# 九、数据绑定和验证
|
||
|
||
## 9.1 数据绑定
|
||
|
||
Gin提供了多种数据绑定方法:
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
type User struct {
|
||
Username string `form:"username" json:"username" binding:"required"`
|
||
Password string `form:"password" json:"password" binding:"required"`
|
||
Age int `form:"age" json:"age" binding:"required,gte=18"`
|
||
Email string `form:"email" json:"email" binding:"required,email"`
|
||
}
|
||
|
||
// 绑定JSON
|
||
r.POST("/json-binding", func(c *gin.Context) {
|
||
var user User
|
||
if err := c.ShouldBindJSON(&user); err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(200, user)
|
||
})
|
||
|
||
// 绑定表单
|
||
r.POST("/form-binding", func(c *gin.Context) {
|
||
var user User
|
||
if err := c.ShouldBind(&user); err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(200, user)
|
||
})
|
||
|
||
// 绑定URI参数
|
||
r.GET("/uri-binding/:name/:age", func(c *gin.Context) {
|
||
type Person struct {
|
||
Name string `uri:"name" binding:"required"`
|
||
Age int `uri:"age" binding:"required,gte=18"`
|
||
}
|
||
|
||
var person Person
|
||
if err := c.ShouldBindUri(&person); err != nil {
|
||
c.JSON(400, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(200, person)
|
||
})
|
||
```
|
||
|
||
## 9.2 自定义验证器
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 自定义验证器
|
||
type Booking struct {
|
||
CheckIn time.Time `form:"check_in" binding:"required,bookabledate"`
|
||
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn"`
|
||
}
|
||
|
||
func bookableDate(fl validator.FieldLevel) bool {
|
||
date, ok := fl.Field().Interface().(time.Time)
|
||
if !ok {
|
||
return false
|
||
}
|
||
|
||
today := time.Now()
|
||
if date.Unix() < today.Unix() {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func setupValidators() {
|
||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||
v.RegisterValidation("bookabledate", bookableDate)
|
||
}
|
||
}
|
||
```
|
||
|
||
# 十、项目实战:待办事项API
|
||
|
||
让我们创建一个简单的待办事项(Todo)API来综合应用Gin框架的各种功能。
|
||
|
||
## 10.1 定义模型
|
||
|
||
```go
|
||
// models/todo.go
|
||
package models
|
||
|
||
import (
|
||
"time"
|
||
)
|
||
|
||
type Todo struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title" binding:"required"`
|
||
Description string `json:"description"`
|
||
Completed bool `json:"completed"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
var todos = []Todo{
|
||
{ID: 1, Title: "学习Gin框架", Description: "掌握Gin框架的基础知识", Completed: false, CreatedAt: time.Now()},
|
||
{ID: 2, Title: "完成项目", Description: "使用Gin框架完成项目开发", Completed: false, CreatedAt: time.Now()},
|
||
}
|
||
|
||
// 获取所有待办事项
|
||
func GetAllTodos() []Todo {
|
||
return todos
|
||
}
|
||
|
||
// 根据ID获取待办事项
|
||
func GetTodoByID(id uint) (Todo, bool) {
|
||
for _, todo := range todos {
|
||
if todo.ID == id {
|
||
return todo, true
|
||
}
|
||
}
|
||
return Todo{}, false
|
||
}
|
||
|
||
// 创建新的待办事项
|
||
func CreateTodo(todo Todo) Todo {
|
||
todo.ID = uint(len(todos) + 1)
|
||
todo.CreatedAt = time.Now()
|
||
todos = append(todos, todo)
|
||
return todo
|
||
}
|
||
|
||
// 更新待办事项
|
||
func UpdateTodo(id uint, updatedTodo Todo) (Todo, bool) {
|
||
for i, todo := range todos {
|
||
if todo.ID == id {
|
||
updatedTodo.ID = id
|
||
updatedTodo.CreatedAt = todo.CreatedAt
|
||
todos[i] = updatedTodo
|
||
return updatedTodo, true
|
||
}
|
||
}
|
||
return Todo{}, false
|
||
}
|
||
|
||
// 删除待办事项
|
||
func DeleteTodo(id uint) bool {
|
||
for i, todo := range todos {
|
||
if todo.ID == id {
|
||
todos = append(todos[:i], todos[i+1:]...)
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
```
|
||
|
||
## 10.2 实现控制器
|
||
|
||
|
||
```go
|
||
// controllers/todo_controller.go
|
||
package controllers
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gin-demo/models"
|
||
)
|
||
|
||
// 获取所有待办事项
|
||
func GetTodos(c *gin.Context) {
|
||
todos := models.GetAllTodos()
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": todos,
|
||
})
|
||
}
|
||
|
||
// 获取单个待办事项
|
||
func GetTodo(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"status": "error",
|
||
"message": "无效的ID",
|
||
})
|
||
return
|
||
}
|
||
|
||
todo, found := models.GetTodoByID(uint(id))
|
||
if !found {
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"status": "error",
|
||
"message": "待办事项不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": todo,
|
||
})
|
||
}
|
||
|
||
// 创建待办事项
|
||
func CreateTodo(c *gin.Context) {
|
||
var todo models.Todo
|
||
|
||
if err := c.ShouldBindJSON(&todo); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"status": "error",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
newTodo := models.CreateTodo(todo)
|
||
|
||
c.JSON(http.StatusCreated, gin.H{
|
||
"status": "success",
|
||
"data": newTodo,
|
||
})
|
||
}
|
||
|
||
// 更新待办事项
|
||
func UpdateTodo(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"status": "error",
|
||
"message": "无效的ID",
|
||
})
|
||
return
|
||
}
|
||
|
||
var todo models.Todo
|
||
if err := c.ShouldBindJSON(&todo); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"status": "error",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
updatedTodo, found := models.UpdateTodo(uint(id), todo)
|
||
if !found {
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"status": "error",
|
||
"message": "待办事项不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"data": updatedTodo,
|
||
})
|
||
}
|
||
|
||
// 删除待办事项
|
||
func DeleteTodo(c *gin.Context) {
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"status": "error",
|
||
"message": "无效的ID",
|
||
})
|
||
return
|
||
}
|
||
|
||
found := models.DeleteTodo(uint(id))
|
||
if !found {
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"status": "error",
|
||
"message": "待办事项不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "success",
|
||
"message": "待办事项已删除",
|
||
})
|
||
}
|
||
```
|
||
|
||
## 10.3 定义路由
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// main.go
|
||
package main
|
||
|
||
import (
|
||
"github.com/gin-gonic/gin"
|
||
"gin-demo/controllers"
|
||
)
|
||
|
||
func setupRouter() *gin.Engine {
|
||
r := gin.Default()
|
||
|
||
// 日志中间件
|
||
r.Use(gin.Logger())
|
||
|
||
// API路由
|
||
api := r.Group("/api")
|
||
{
|
||
todos := api.Group("/todos")
|
||
{
|
||
todos.GET("", controllers.GetTodos)
|
||
todos.GET("/:id", controllers.GetTodo)
|
||
todos.POST("", controllers.CreateTodo)
|
||
todos.PUT("/:id", controllers.UpdateTodo)
|
||
todos.DELETE("/:id", controllers.DeleteTodo)
|
||
}
|
||
}
|
||
|
||
return r
|
||
}
|
||
|
||
func main() {
|
||
r := setupRouter()
|
||
r.Run(":8080")
|
||
}
|
||
```
|
||
|
||
## 10.4 测试API
|
||
|
||
可以使用Postman或curl测试API:
|
||
|
||
bash复制代码
|
||
|
||
```bash
|
||
# 获取所有待办事项
|
||
curl http://localhost:8080/api/todos
|
||
|
||
# 获取单个待办事项
|
||
curl http://localhost:8080/api/todos/1
|
||
|
||
# 创建待办事项
|
||
curl -X POST http://localhost:8080/api/todos \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"title":"测试Gin API","description":"使用curl测试Gin API功能"}'
|
||
|
||
# 更新待办事项
|
||
curl -X PUT http://localhost:8080/api/todos/1 \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"title":"学习Gin框架","description":"掌握Gin框架的基础知识","completed":true}'
|
||
|
||
# 删除待办事项
|
||
curl -X DELETE http://localhost:8080/api/todos/3
|
||
```
|
||
|
||
# 十一、部署与最佳实践
|
||
|
||
## 11.1 生产环境部署
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 设置生产模式
|
||
gin.SetMode(gin.ReleaseMode)
|
||
|
||
// 创建自定义HTTP服务器
|
||
s := &http.Server{
|
||
Addr: ":8080",
|
||
Handler: router,
|
||
ReadTimeout: 10 * time.Second,
|
||
WriteTimeout: 10 * time.Second,
|
||
MaxHeaderBytes: 1 << 20, // 1 MB
|
||
}
|
||
|
||
// 启动服务器
|
||
s.ListenAndServe()
|
||
```
|
||
|
||
## 11.2 优雅关闭
|
||
|
||
go复制代码
|
||
|
||
```go
|
||
// 优雅关闭服务器
|
||
func main() {
|
||
router := setupRouter()
|
||
|
||
srv := &http.Server{
|
||
Addr: ":8080",
|
||
Handler: router,
|
||
}
|
||
|
||
// 在一个新的goroutine中启动服务器
|
||
go func() {
|
||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
log.Fatalf("listen: %s\n", err)
|
||
}
|
||
}()
|
||
|
||
// 等待中断信号以优雅地关闭服务器
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
log.Println("正在关闭服务器...")
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
if err := srv.Shutdown(ctx); err != nil {
|
||
log.Fatal("服务器强制关闭:", err)
|
||
}
|
||
|
||
log.Println("服务器已优雅关闭")
|
||
}
|
||
```
|
||
|
||
## 11.3 性能优化建议
|
||
|
||
1. **使用合适的并发模型**:Go的goroutine很轻量,但过多会消耗资源
|
||
2. **优化数据库查询**:使用索引、连接池
|
||
3. **实现缓存机制**:使用Redis等缓存热门数据
|
||
4. **启用GZIP压缩**:减少传输数据量
|
||
5. **使用适当的日志级别**:生产环境中减少不必要的日志
|
||
6. **定期进行性能测试**:使用工具如Apache Bench或Vegeta
|
||
|
||
# 十二、总结
|
||
|
||
Gin是一个功能强大且性能卓越的Go Web框架,适合构建各种类型的Web应用和API服务。本教程涵盖了Gin的基础知识和实际应用,包括路由、中间件、请求处理、响应生成等方面。
|
||
|
||
通过学习和实践,您应该能够使用Gin框架开发出高效、可靠的Web应用程序。继续深入学习和探索Gin的更多高级特性,将有助于您成为一名出色的Go Web开发者。
|
||
|
||
# 十三、课堂练习
|
||
|
||
## (一)、项目概述
|
||
|
||
开发简单的学生信息管理系统的接口,用于对学生的基本信息进行增删改查操作,以此熟悉和加深 Gin 框架的基础使用,包括路由设置、请求处理、数据绑定等知识。
|
||
|
||
## (二)、功能需求
|
||
|
||
### 学生信息结构体定义
|
||
|
||
定义一个 Student 结构体,包含以下字段:
|
||
|
||
- ID:学生 ID,整数类型
|
||
- Name:学生姓名,字符串类型
|
||
- Age:学生年龄,整数类型
|
||
- Grade:学生年级,字符串类型
|
||
|
||
### 路由与请求处理
|
||
|
||
1. **创建学生信息(POST 请求)**
|
||
|
||
- **路由**:`/students`
|
||
- **功能**:接收包含学生信息的 JSON 数据,将其绑定到 Student 结构体,在控制台打印学生信息,并返回创建成功的消息。
|
||
2. **获取所有学生信息(GET 请求)**
|
||
|
||
- **路由**:`/students`
|
||
- **功能**:模拟从数据库或数据存储中获取所有学生信息(可以简单用切片存储数据),将学生信息以 JSON 格式返回。如果没有学生信息,返回一个空的学生信息列表。
|
||
3. **获取单个学生信息(GET 请求)**
|
||
|
||
- **路由**:`/students/:id`
|
||
- **功能**:根据 URL 中的 id 参数获取对应的学生信息。如果找到,返回该学生信息;如果未找到,返回错误消息。
|
||
4. **更新学生信息(PUT 请求)**
|
||
|
||
- **路由**:`/students/:id`
|
||
- **功能**:根据 URL 中的 `id` 参数找到对应的学生信息,接收包含更新后学生信息的 JSON 数据,将其绑定到 `Student` 结构体,更新学生信息(在控制台打印更新后的信息),并返回更新成功的消息。如果未找到指定 `id` 的学生信息,返回错误消息。
|
||
5. **删除学生信息(DELETE 请求)**
|
||
|
||
- **路由**:`/students/:id`
|
||
- **功能**:根据 URL 中的 `id` 参数找到对应的学生信息并删除(可以简单在内存中模拟删除,如从切片中移除该学生信息)。如果删除成功,返回删除成功的消息;如果未找到指定 `id` 的学生信息,返回错误消息。
|
||
|
||
## (三)、项目结构要求
|
||
|
||
1. **`main.go`**:作为项目入口,初始化 Gin 引擎,加载所有路由。
|
||
2. **`models` 目录**:在该目录下创建 `student.go` 文件,定义 `Student` 结构体。
|
||
3. **`routes` 目录**:在该目录下创建 `student.go` 文件,定义所有与学生信息相关的路由。
|
||
4. **`controllers` 目录**:在该目录下创建 `student.go` 文件,编写处理学生信息相关请求的具体逻辑。
|
||
|
||
## (四)、其他要求
|
||
|
||
1. 目录: `week03/practice/gin/`
|
||
2. 合理处理请求过程中的错误,如数据绑定失败、找不到资源等情况,并返回合适的 HTTP 状态码和错误信息。
|
||
3. 在控制台打印关键的操作信息,如接收到的请求、处理结果等,方便调试和观察系统运行情况。
|
||
|
||
## (五)进阶部分
|
||
|
||
1. **版本划分**:将上述实现的接口定义为 v1 版本。在此基础上,开发 v2 和 v3 版本接口。
|
||
2. **v2 版本**:使用 SQLite 数据库,通过原始 SQL 语句实现学生信息的增删改查功能。路由以 /v2 开头,与 v1 版本对应功能的路由类似,如 /v2/students 等。需要处理好数据库连接、SQL 语句执行及错误处理等操作。
|
||
3. **v3 版本**:使用 Mysql 数据库,但借助 GORM 库实现学生信息的增删改查功能。路由以 /v3 开头,例如 /v3/students 等。要完成 GORM 的配置、模型定义及相关数据库操作方法的调用,同时做好错误处理。
|
||
4. **v4 版本**:在 GORM 操作 MySQL 实现学生信息增删改查基础上,在获取学生信息(包括获取单个与所有学生信息)时使用 Redis 缓存,为缓存数据设过期时间(如 5 分钟),增改操作后清理相关缓存以保证数据一致性 。
|
||
5. **项目结构调整**:在 routes 和 controllers 目录下分别创建 v2 和 v3 子目录,存放对应版本的路由和请求处理逻辑代码。在 db 目录下创建数据库连接相关代码,供 v2 和 v3 版本使用。
|
||
6. **错误处理与日志**:各版本均需合理处理请求过程中的各类错误,返回合适的 HTTP 状态码和错误信息,并在控制台打印关键操作信息以便调试。
|
||
|
||
# 参考资源
|
||
|
||
- [Gin官方文档](https://gin-gonic.com/docs/)
|
||
- [Gin GitHub仓库](https://github.com/gin-gonic/gin)
|
||
- [Go官方文档](https://golang.org/doc/)
|
||
- [Go Web编程](https://astaxie.gitbooks.io/build-web-application-with-golang/content/zh/) |