All writing Notes & observations

Gin Tutorial

前言

欢迎来到 Gin 框架的零基础教程!无论你是刚接触 Go 语言的新手,还是想要快速上手 Web 开发的开发者,这篇教程都会带你从零开始,一步步掌握 Go 语言中最流行的轻量级 Web 框架 ——Gin。

什么是 Gin?

Gin 是一个用 Go (Golang) 编写的高性能 HTTP Web 框架。它提供了类似 Express.js 的简洁 API,同时凭借其基于 Radix 树的路由算法,实现了极致的性能,速度比标准的 net/http 库快了多达 40 倍。

为什么选择 Gin?

  • 🚀 极致性能:零分配路由,内存占用极低,支持高并发

  • 📦 轻量简洁:核心代码精简,无冗余依赖,上手成本低

  • 🔌 丰富中间件:内置日志、恢复、CORS 等中间件,支持自定义扩展

  • ✅ 参数校验:自动绑定并验证请求参数,减少重复代码

  • 🎯 路由分组:灵活的路由组织方式,支持 API 版本管理

  • 🛡️ panic 恢复:内置异常捕获,防止单个请求崩溃导致整个服务挂掉

前置知识

在开始之前,你只需要:

  1. 了解基本的计算机操作(会用终端 / 命令行)

  2. 了解 Go 语言的基础语法(变量、函数、结构体等)

  3. 了解基本的 HTTP 协议概念(请求、响应、GET/POST 方法等)


第一章:环境准备与安装

1.1 安装 Go 语言环境

Gin 框架依赖 Go 语言环境,首先我们需要安装 Go。

1.1.1 下载 Go 安装包

访问 Go 官方下载页面:https://go.dev/dl/

根据你的操作系统选择对应的安装包:

  • Windows:选择 go1.2X.X.windows-amd64.msi(推荐 64 位)

  • MacOS:选择 go1.2X.X.darwin-amd64.pkg(Intel 芯片)或 go1.2X.X.darwin-arm64.pkg(M1/M2 芯片)

  • Linux:选择 go1.2X.X.linux-amd64.tar.gz

💡 提示:Gin v1.10+ 要求 Go 版本至少为 1.25,建议安装最新的稳定版。

1.1.2 安装 Go

Windows 用户:

  • 双击下载的 go.msi 文件,一路点击 Next 即可。安装程序会自动配置环境变量,无需手动设置。

MacOS 用户:

  • 双击下载的 .pkg 文件,按照提示完成安装。同样会自动配置环境变量。

Linux 用户:

1
2
3
4
5
6
# 解压到 /usr/local 目录
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz

# 配置环境变量,编辑 ~/.bashrc 或 ~/.zshrc
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

1.1.3 验证安装

打开终端,输入以下命令验证是否安装成功:

1
go version

如果输出类似下面的内容,说明安装成功:

1
go version go1.25.0 linux/amd64

1.2 配置 Go 模块(Go Modules)

从 Go 1.11 开始,Go 引入了模块(Modules)来管理依赖,这是现在官方推荐的依赖管理方式,我们必须启用它。

💡 国内用户注意:由于网络原因,直接下载依赖可能会超时,我们先配置国内代理:

1
2
3
4
5
6
7
8
9
# 设置 GOPROXY 代理(七牛云镜像,国内访问速度快)
# Windows PowerShell
$env:GOPROXY = "https://goproxy.cn,direct"

# Windows CMD
set GOPROXY=https://goproxy.cn,direct

# Linux/Mac
export GOPROXY=https://goproxy.cn,direct

如果你想永久生效,可以把上面的命令添加到你的系统环境变量配置文件中。

1.3 安装 Gin 框架

现在我们来创建第一个 Gin 项目,并安装 Gin 框架。

1.3.1 创建项目目录

首先,创建一个项目文件夹,比如 my-gin-app:

1
2
3
4
5
# 创建项目目录
mkdir my-gin-app

# 进入项目目录
cd my-gin-app

1.3.2 初始化 Go 模块

在项目目录下,初始化 Go 模块:

1
go mod init my-gin-app

这个命令会创建一个 go.mod 文件,用来管理你的项目依赖。执行成功后,你会看到输出:

1
2
3
go: creating new go.mod: module my-gin-app
go: to add module requirements and sums:
	go mod tidy

1.3.3 安装 Gin

执行以下命令安装 Gin 框架:

1
go get -u github.com/gin-gonic/gin

这个命令会:

  1. 下载 Gin 框架的最新版本

  2. 自动把依赖信息添加到 go.mod 文件中

  3. 生成 go.sum 文件,用来校验依赖的版本

等待命令执行完成,没有报错的话,Gin 就安装好了!


第二章:你的第一个 Gin 应用

2.1 编写 Hello World

现在我们来写第一个最简单的 Gin 程序,实现一个 ping 接口。

在项目根目录下,创建一个 main.go 文件,写入以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

// 导入必要的包
import (
	"net/http" // 标准库,用于 HTTP 状态码
	"github.com/gin-gonic/gin" // 导入 Gin 框架
)

func main() {
	// 1. 创建 Gin 引擎
	// gin.Default() 会创建一个默认的引擎,自带两个中间件:
	// - Logger:打印请求日志
	// - Recovery:捕获 panic,防止服务崩溃
	r := gin.Default()

	// 2. 定义路由
	// 当客户端用 GET 方法访问 /ping 路径时,执行后面的处理函数
	r.GET("/ping", func(c *gin.Context) {
		// c *gin.Context 是 Gin 的核心,它包含了请求和响应的所有信息

		// 3. 返回 JSON 响应
		// 第一个参数是 HTTP 状态码,http.StatusOK 就是 200
		// 第二个参数是响应数据,gin.H 是 map[string]interface{} 的快捷方式
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
			"status":  "success",
		})
	})

	// 4. 启动服务器
	// 默认监听 0.0.0.0:8080,你也可以指定端口,比如 r.Run(":9090")
	r.Run() 
}

2.2 运行程序

在终端中,执行以下命令运行程序:

1
go run main.go

如果一切正常,你会看到类似下面的输出:

1
2
3
4
5
6
7
8
9
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080

这说明你的 Gin 服务已经成功启动了,正在监听 8080 端口!

2.3 测试接口

现在打开你的浏览器,访问 http://localhost:8080/ping,或者用 curl.exe 命令测试:

1
curl.exe http://localhost:8080/ping

你会看到返回的 JSON 数据:

1
{"message":"pong","status":"success"}

🎉 恭喜!你已经成功运行了你的第一个 Gin 应用!


第三章:核心功能:路由管理

路由是 Web 框架的基础,它决定了如何响应客户端的请求。Gin 提供了非常强大灵活的路由功能。

3.1 基础路由

Gin 支持所有标准的 HTTP 方法:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// GET请求:获取资源
	r.GET("/users", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "获取用户列表"})
	})

	// POST请求:创建资源
	r.POST("/users", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "创建新用户"})
	})

	// PUT请求:更新资源(完整更新)
	r.PUT("/users/:id", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "更新用户信息"})
	})

	// DELETE请求:删除资源
	r.DELETE("/users/:id", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "删除用户"})
	})

	// Any:匹配所有请求方法
	r.Any("/test", func(c *gin.Context) {
		c.String(http.StatusOK, "支持所有请求方法:%s", c.Request.Method)
	})

	r.Run()
}

可以用 Postman 或者 curl.exe 来测试这些接口:

1
2
3
4
5
6
curl.exe -X GET http://localhost:8080/users
curl.exe -X POST http://localhost:8080/users
curl.exe -X PUT http://localhost:8080/users/123
curl.exe -X DELETE http://localhost:8080/users/123
curl.exe -X PATCH http://localhost:8080/users/123
curl.exe -X OPTIONS http://localhost:8080/test

3.2 路径参数(Route Parameters)

有时候我们需要从 URL 路径中获取动态参数,比如用户 ID、文章 ID 等。Gin 支持用 :param 的方式定义动态参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/users/:id", func(c *gin.Context) {
		// 使用 c.Param() 获取路径参数
		userID := c.Param("id")

		c.JSON(http.StatusOK, gin.H{
			"user_id": userID,
		})
	})

	// 多个参数
	r.GET("/articles/:category/:article_id", func(c *gin.Context) {
		category := c.Param("category")
		articleID := c.Param("article_id")

		c.JSON(http.StatusOK, gin.H{
			"category":  category,
			"articleID": articleID,
		})
	})

	r.Run()
}

测试一下:

1
2
3
4
5
curl.exe http://localhost:8080/users/123
# 输出:{"user_id":"123"}

curl.exe http://localhost:8080/articles/tech/456
# 输出:{"article_id":"456","category":"tech"}

3.3 查询参数(Query Parameters)

查询参数是 URL 中 ? 后面的键值对,比如 /search?query=gin&page=1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
r.GET("/search", func(c *gin.Context) {
	// 获取查询参数,如果参数不存在,返回空字符串
	query := c.Query("query")
	
	// 获取查询参数,带默认值,如果参数不存在,返回默认值
	page := c.DefaultQuery("page", "1")
	limit := c.DefaultQuery("limit", "10")
	
	c.JSON(http.StatusOK, gin.H{
		"query": query,
		"page":  page,
		"limit": limit,
	})
})

测试一下:

1
2
curl.exe "http://localhost:8080/search?query=golang&page=2"
# 输出:{"limit":"10","page":"2","query":"golang"}

3.4 路由分组(Route Grouping)

当你的 API 越来越多的时候,你需要对路由进行分类管理。比如 API 版本、模块划分。Gin 的路由分组可以帮你轻松实现这一点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// 创建 v1 版本的 API 分组
	v1 := r.Group("/api/v1")
	{
		// 实际路径是 /api/v1/users
		v1.GET("/users", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "v1 用户列表"})
		})

		// 实际路径是 /api/v1/users/:id
		v1.GET("users/:id", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "v1 用户详情"})
		})
	}

	// 创建 v2 版本的 API 分组
	v2 := r.Group("/api/v2")
	{
		// 实际路径是 /api/v2/users
		v2.GET("/users", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "v2 用户列表"})
		})

		// 实际路径是 /api/v2/users/:id
		v2.GET("users/:id", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "v2 用户详情"})
		})
	}

	// 嵌套分组:比如 /api/v1/admin
	admin := v1.Group("/admin")
	{
		admin.GET("/dashboard", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "管理员面板"})
		})
	}

	r.Run()
}

路由分组的好处是:

  1. 代码结构更清晰,便于维护

  2. 可以给整个分组统一应用中间件(后面会讲)

  3. 支持无限嵌套,满足复杂的路由组织需求

3.5 404 处理

当用户访问不存在的路由时,Gin 默认会返回 404 页面。我们可以自定义 404 的处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.NoRoute(func(c *gin.Context) {
		c.JSON(http.StatusNotFound, gin.H{
			"code":    404,
			"message": "接口被陈导偷走了,去问问陈导吧",
		})
	})

	r.Run()
}


第四章:请求处理与参数绑定

处理客户端发来的请求数据是后端开发的核心任务。Gin 提供了非常便捷的参数绑定和验证功能。

4.1 获取表单数据

处理 HTML 表单提交(application/x-www-form-urlencoded):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.POST("/login", func(c *gin.Context) {
		// 获取表单数据
		username := c.PostForm("username")
		password := c.DefaultPostForm("password", "123456")

		c.JSON(http.StatusOK, gin.H{
			"username": username,
			"password": password,
		})
	})

	r.Run()
}

测试:

1
2
curl.exe -X POST http://localhost:8080/login -d "username=test&password=123"
# 输出:{"password":"123","username":"test"}

4.2 JSON 参数绑定

现代 API 开发最常用的就是 JSON 格式的请求体。Gin 可以自动把 JSON 请求体绑定到 Go 的结构体上,还能自动校验参数!

首先,我们定义一个结构体,用来接收请求参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// 定义注册请求的结构体
type RegisterRequest struct {
	// json:"username"	表示 JSON 中的字段名
	// binding:"required"	表示这个字段是必填的
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required,min=6"` // 密码至少6位
	Email    string `json:"email" binding:"required,email"`    // 必须是邮箱格式
	Age      int    `json:"age" binding:"gte=18,lte=60"`       // 年龄必须在18-60之间
}

func main() {
	r := gin.Default()

	// 然后在处理函数中绑定参数:
	r.POST("/register", func(c *gin.Context) {
		var req RegisterRequest

		// ShouldBindJSON 会自动解析JSON请求体,绑定到结构体,并执行校验
		if err := c.ShouldBindJSON(&req); err != nil {
			// 如果绑定或校验失败,返回400错误
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		// 绑定成功
		c.JSON(http.StatusOK, gin.H{
			"message": "注册成功",
			"data":    req,
		})
	})

	r.Run()
}

测试一下:

1
2
3
4
# 正确的请求
curl.exe -X POST http://localhost:8080/register -H "Content-Type: application/json" -d '{\"username\":\"test\",\"password\":\"123456\",\"email\":\"test@example.com\",\"age\":20}'

# 输出:{"data":{"username":"test","password":"123456","email":"test@example.com","age":20},"message":"注册成功"}
  • -X POST 表示使用 POST 方法
  • -H “Content-Type: application/json” 表示请求体是 JSON 格式
  • -d 后面是 JSON 数据。

1
2
3
4
# 错误的请求(年龄太小)
curl.exe -X POST http://localhost:8080/register -H "Content-Type: application/json" -d '{\"username\":\"test\",\"password\":\"123456\",\"email\":\"test@example.com\",\"age\":10}' 

# 输出:{"error":"Key: 'RegisterRequest.Age' Error:Field validation for 'Age' failed on the 'gte' tag"}

💡 常用的 binding 校验规则:

  • required:必填

  • email:邮箱格式

  • min=6:最小值(数字)/ 最小长度(字符串)

  • max=60:最大值

  • gte=18:大于等于

  • lte=60:小于等于

  • oneof=male female:必须是其中一个值

4.3 Query 参数绑定

除了 JSON,我们也可以把 URL 的查询参数绑定到结构体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type UserQuery struct {
	Name string `form:"name" binding:"required"`
	Age  int    `form:"age" binding:"min=18"`
}

func main() {
	r := gin.Default()

	r.GET("/users", func(c *gin.Context) {
		var query UserQuery
		if err := c.ShouldBindQuery(&query); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{"query": query})
	})

	r.Run()
}

测试:

1
2
curl.exe "http://localhost:8080/users?name=Alice&age=25"
# 输出:{"query":{"Name":"Alice","Age":25}}

4.4 文件上传

Gin 也支持文件上传,非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"log"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.POST("/upload", func(c *gin.Context) {
		// 获取上传的文件,参数是表单的name
        // c.FormFile() 会返回一个 *multipart.FileHeader 类型的对象,包含了文件的基本信息,参数是表单的 name 属性值
		file, err := c.FormFile("file")
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 打印文件信息
		log.Printf("上传文件名: %s, 文件大小: %d KB", file.Filename, file.Size/1024)

		// 文件保存到本地
		dst := "./uploads/" + file.Filename
		// 确保 upload 目录存在。os.ModePerm 是 0777,表示所有用户都有读写执行权限,系统会根据 umask 自动调整权限。
		if err := os.MkdirAll("./uploads", os.ModePerm); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "目录创建失败"})
		}

		if err := c.SaveUploadedFile(file, dst); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "文件保存失败"})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"message":  "上传成功",
			"filename": file.Filename,
		})
	})

	r.Run()
}

测试:

1
curl.exe -X POST http://localhost:8080/upload -F "file=@这是个测试文件.txt"


第五章:响应处理

Gin 支持多种格式的响应,满足不同的场景需求。

5.1 JSON 响应(最常用)

这是 API 开发最常用的响应格式,我们之前已经用过很多次了:

1
2
3
4
5
c.JSON(http.StatusOK, gin.H{
	"code":    200,
	"message": "success",
	"data":    data,
})

5.2 字符串响应

简单的文本响应:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/hello", func(c *gin.Context) {
		name := c.DefaultQuery("name", "Guest")
		c.String(http.StatusOK, "Hello, %s!", name)
	})

	r.Run()
}

测试:

1
curl.exe "http://localhost:8080/hello"  

5.3 HTML 模板渲染

如果你需要服务端渲染 HTML 页面,Gin 也支持 HTML 模板:

首先,创建 templates 目录,然后创建 index.html 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
<head>
    {{.title}}
</head>
<body>
    <h1>{{.message}}</h1>
    <p>欢迎你,{{.name}}</p>
</body>
</html>

然后在 Gin 中加载模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// 加载 templates 目录下的所有 html 文件
	r.LoadHTMLGlob("templates/*")

	r.GET("/index", func(c *gin.Context) {
		// 渲染模板,传递参数
		c.HTML(http.StatusOK, "index.html", gin.H{
			"title":   "我的主页",
			"message": "欢迎来到 Gin 的世界!",
			"name":    "Gopher",
		})
	})

	r.Run()
}

测试:

1
curl.exe http://localhost:8080/index

也可以在浏览器中访问 http://localhost:8080/index,你会看到渲染后的 HTML 页面。

5.4 静态文件服务

当你有静态资源(比如 CSS、JS、图片,或者前端打包后的文件),可以用 Gin 快速托管:

1
2
3
4
// 把 /static 路径映射到本地的 ./static 目录
r.Static("/static", "./static")

// 比如你有 ./static/logo.png,就可以通过 http://localhost:8080/static/logo.png 访问

如果是前后端分离的项目,前端打包后的 dist 目录可以这样托管:

1
r.Static("/", "./dist")

5.5 文件下载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()

	r.GET("/download", func(c *gin.Context) {
		// 直接返回文件给客户端下载
		c.File("./test.txt")
	})

	r.Run()
}

5.6 重定向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/old", func(c *gin.Context) {
		// 301 永久重定向(不推荐)
		// c.Redirect(http.StatusMovedPermanently, "https://symphonyw.github.io/")

		// 302 临时重定向(默认)
		c.Redirect(http.StatusFound, "https://symphonyw.github.io/")

		// 307 临时重定向,保持请求方法不变(推荐)
		c.Redirect(http.StatusTemporaryRedirect, "https://symphonyw.github.io/")
	})

	r.Run()
}

测试:

1
curl.exe -I http://localhost:8080/old

或者直接在浏览器中访问 http://localhost:8080/old,你会被重定向到我的博客主页。


第六章:中间件(Middleware)

中间件是 Gin 中非常强大的特性,它可以在请求到达处理函数之前或之后执行一些通用的逻辑。比如日志记录、身份认证、跨域处理、权限校验等。

6.1 中间件的工作原理

中间件本质上就是一个拦截器,请求会先经过中间件,然后才到你的业务处理函数,处理完之后,响应还会再经过中间件。

你可以把它想象成安检:乘客(请求)先过安检(中间件),然后才能进去坐飞机(业务处理),出来的时候还要再过安检。

6.2 默认中间件

还记得我们之前用的 gin.Default() 吗?它默认就帮我们加了两个中间件:

  • gin.Logger():打印请求日志,比如请求方法、路径、耗时、状态码等

  • gin.Recovery():捕获 panic,防止因为一个请求的错误导致整个服务崩溃

如果你想要一个完全干净的引擎,没有任何中间件,可以用 gin.New():

1
2
3
4
r := gin.New()
// 然后你可以手动添加你需要的中间件
r.Use(gin.Logger())
r.Use(gin.Recovery())

6.3 自定义中间件

我们来写一个自定义的日志中间件,看看中间件是怎么工作的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 自定义日志中间件
func LoggerMiddleware() gin.HandlerFunc {
	// 返回一个 gin.HandlerFunc 类型的函数
	return func(c *gin.Context) {
		// 请求处理之前执行的逻辑
		startTime := time.Now()

		// 调用下一个处理函数(可能是下一个中间件,也可能是最终的业务处理函数)
		c.Next()

		// 请求处理之后执行的逻辑
		endTime := time.Now()
		latency := endTime.Sub(startTime) // 计算耗时

		// 获取请求信息
		method := c.Request.Method
		path := c.Request.URL.Path
		statusCode := c.Writer.Status() // 响应状态码
		clientIP := c.ClientIP()       // 客户端 IP

		// 打印日志
		log.Printf("[%s] %s | %d | %s | %v", method, path, statusCode, clientIP, latency)
	}
}

然后我们就可以使用这个中间件了:

1
2
3
4
5
6
7
8
func main() {
	r := gin.Default()
	
	// 全局使用:对所有路由生效
	r.Use(LoggerMiddleware())

	// ... 定义路由 ...
}

6.4 中间件的作用范围

中间件可以作用在不同的范围:

  1. 全局中间件:对所有路由生效,用 r.Use()

  2. 分组中间件:只对某个路由分组生效,用 group.Use()

  3. 单个路由中间件:只对某个特定的路由生效,直接在路由定义时添加中间件参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 认证中间件示例
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从请求头获取 Token
		token := c.GetHeader("Authorization")
		
		// 简单的验证(实际项目中要验证 JWT)
		if token != "Bearer my-secret-token" {
			// 验证失败,终止请求链,返回 401
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"error": "未授权访问,请先登录",
			})
			return
		}

		// 验证通过,把用户信息存到 Context 中,供后续处理函数使用
		c.Set("userID", 123)
		c.Set("username", "test")

		// 继续处理请求
		c.Next()
	}
}

func main() {
	r := gin.Default()

	// 1. 全局中间件
	r.Use(LoggerMiddleware())

	// 公开路由,不需要认证
	r.POST("/login", loginHandler)

	// 2. 分组中间件:需要认证的接口
	authGroup := r.Group("/api")
	authGroup.Use(AuthMiddleware()) // 这个分组下的所有路由都需要认证
	{
		authGroup.GET("/profile", profileHandler)
		authGroup.POST("/update", updateHandler)
	}

	// 3. 单个路由中间件
	r.GET("/special", AuthMiddleware(), specialHandler)

	r.Run()
}

💡 关键方法说明:

  • c.Next():调用下一个处理函数,继续执行链

  • c.Abort():终止执行链,后面的处理函数不会再执行

  • c.Set(key, value) / c.Get(key):在中间件和处理函数之间传递数据

6.5 常用社区中间件:解决跨域问题

开发前后端分离项目的时候,最常见的问题就是跨域(CORS)。

什么是跨域?当你的前端页面和后端 API 不在同一个域名、协议或端口下时,浏览器会出于安全考虑阻止前端访问后端接口,这就是跨域问题。

我们可以用 gin-contrib/cors 这个官方的中间件来解决。

首先安装:

1
go get github.com/gin-contrib/cors

然后使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "github.com/gin-contrib/cors"

func main() {
	r := gin.Default()

	// 配置 CORS 中间件
	r.Use(cors.New(cors.Config{
		// 允许的来源,比如前端的域名
		AllowOrigins:     []string{"http://localhost:3000", "https://your-frontend.com"},
		// 允许的请求方法
		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
		// 允许的请求头
		AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
		// 是否允许携带 Cookie
		AllowCredentials: true,
		// 预检请求的缓存时间
		MaxAge:           12 * time.Hour,
	}))

	// 如果你是开发环境,想快速测试,可以用默认配置(允许所有来源,不推荐生产环境)
	// r.Use(cors.Default())

	// ... 路由 ...
}

这样就完美解决了跨域问题!


第七章:配置管理

在实际项目中,我们不会把配置(比如数据库地址、端口、密钥)硬编码在代码里,而是用配置文件来管理。最常用的就是 Viper 库。

7.1 安装 Viper

1
go get github.com/spf13/viper

7.2 编写配置文件

在项目根目录创建 config.yaml 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 服务配置
server:
  port: 8080
  mode: debug # debug/release

# 数据库配置
database:
  host: localhost
  port: 3306
  user: root
  password: 123456
  dbname: myapp

# JWT 配置
jwt:
  secret: my-secret-key
  expire: 24 # 过期时间,小时

7.3 读取配置

创建 config/config.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package config

import (
	"log"

	"github.com/spf13/viper"
)

// 定义配置结构体
type Config struct {
	Server   ServerConfig
	Database DatabaseConfig
	JWT      JWTConfig
}

type ServerConfig struct {
	Port int
	Mode string
}

type DatabaseConfig struct {
	Host     string
	Port     int
	User     string
	Password string
	DBName   string
}

type JWTConfig struct {
	Secret string
	Expire int
}

var GlobalConfig Config

// 加载配置
func InitConfig() {
	// 设置配置文件名(不带后缀)
	viper.SetConfigName("config")
	// 设置配置文件路径
	viper.AddConfigPath(".")
	// 设置配置文件类型
	viper.SetConfigType("yaml")

	// 读取配置文件
	if err := viper.ReadInConfig(); err != nil {
		log.Fatalf("读取配置文件失败: %v", err)
	}

	// 把配置反序列化到结构体
	if err := viper.Unmarshal(&GlobalConfig); err != nil {
		log.Fatalf("解析配置失败: %v", err)
	}

	log.Println("配置加载成功")
}

然后在 main.go 中初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	// 加载配置
	config.InitConfig()

	r := gin.Default()

	// 从配置中获取端口
	port := config.GlobalConfig.Server.Port
	r.Run(fmt.Sprintf(":%d", port))
}

这样,你就可以轻松管理不同环境的配置了,开发环境用 config.dev.yaml,生产环境用 config.prod.yaml。


第八章:身份认证:JWT

在 API 开发中,JWT(JSON Web Token)是最常用的身份认证方式。

JWT 是一种基于 Token 的认证机制,客户端登录成功后会获得一个 JWT Token,之后每次请求都携带这个 Token,服务器验证 Token 的合法性来判断用户是否有权限访问资源。

我们来看看怎么在 Gin 中使用 JWT。

8.1 安装依赖

1
go get github.com/golang-jwt/jwt/v5

8.2 实现 JWT 工具函数

创建 utils/jwt.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package utils

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"my-gin-app/config"
)

// 定义 Claims,就是 Token 中存储的信息
type Claims struct {
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
	jwt.RegisteredClaims
}

// 生成 Token
func GenerateToken(userID uint, username string) (string, error) {
	// 设置过期时间
	expireTime := time.Now().Add(time.Duration(config.GlobalConfig.JWT.Expire) * time.Hour)

	// 创建 Claims
	claims := Claims{
		UserID:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expireTime),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "my-gin-app",
		},
	}

	// 生成 Token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	signedToken, err := token.SignedString([]byte(config.GlobalConfig.JWT.Secret))

	return signedToken, err
}

// 验证 Token
func ParseToken(tokenString string) (*Claims, error) {
	// 解析 Token
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(config.GlobalConfig.JWT.Secret), nil
	})

	if err != nil {
		return nil, err
	}

	// 验证 Claims
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}

	return nil, errors.New("无效的 Token")
}

8.3 JWT 认证中间件

然后我们就可以写 JWT 认证中间件了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取 Token
		tokenString := c.GetHeader("Authorization")
		if tokenString == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
			return
		}

		// 去掉 Bearer 前缀
		tokenString = strings.TrimPrefix(tokenString, "Bearer ")

		// 验证 Token
		claims, err := utils.ParseToken(tokenString)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token 无效或已过期"})
			return
		}

		// 把用户信息存到 Context 中
		c.Set("userID", claims.UserID)
		c.Set("username", claims.Username)

		c.Next()
	}
}

8.4 登录接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func LoginHandler(c *gin.Context) {
	var req struct {
		Username string `json:"username" binding:"required"`
		Password string `json:"password" binding:"required"`
	}

	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 这里简化了,实际要去数据库查询用户,验证密码
	if req.Username == "admin" && req.Password == "123456" {
		// 生成 Token
		token, err := utils.GenerateToken(1, "admin")
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "生成 Token 失败"})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"token": token,
		})
		return
	}

	c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
}

这样,你的 API 就有了完整的身份认证功能!


第九章:实战:用户管理 API

现在我们把前面学的所有知识整合起来,做一个完整的用户管理 API 项目。

9.1 项目结构

我们来规划一下项目的目录结构,这是一个标准的分层架构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
my-gin-app/
├── main.go              # 程序入口
├── go.mod               # 依赖管理
├── go.sum
├── config.yaml          # 配置文件
├── config/              # 配置相关
│   └── config.go
├── controllers/         # 控制器,处理请求
│   └── user_controller.go
├── middleware/          # 中间件
│   ├── auth.go
│   └── logger.go
├── models/              # 数据模型
│   └── user.go
├── utils/               # 工具函数
│   └── jwt.go
└── uploads/             # 上传文件目录

9.2 完整代码实现

我们来实现一个完整的用户管理 API,包含:

  • 登录接口

  • 获取用户列表

  • 获取用户详情

  • 创建用户

  • 更新用户

  • 删除用户

完整的代码就不全部贴在这里了,你可以参考前面的知识点,把它们组合起来。最终的效果是:

  • 公开接口:登录

  • 私有接口:需要 JWT 认证才能访问

  • 统一的参数校验

  • 统一的错误处理

  • 日志记录


第十章:单元测试

写完代码之后,我们需要测试我们的接口。Gin 提供了很方便的测试方式。

创建 main_test.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
)

func TestPingRoute(t *testing.T) {
	// 设置为测试模式
	gin.SetMode(gin.TestMode)

	// 初始化路由
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "pong"})
	})

	// 创建测试请求
	req, _ := http.NewRequest("GET", "/ping", nil)
	w := httptest.NewRecorder()

	// 执行请求
	r.ServeHTTP(w, req)

	// 断言结果
	assert.Equal(t, 200, w.Code)
	assert.Equal(t, `{"message":"pong"}`, w.Body.String())
}

然后运行测试:

1
go test -v

第十一章:生产环境部署

当你的项目开发完成,就可以部署到生产环境了。

11.1 切换到 Release 模式

Gin 默认是 Debug 模式,会打印很多调试信息。生产环境要切换到 Release 模式:

1
2
// 在 main 函数最开始
gin.SetMode(gin.ReleaseMode)

或者通过环境变量:

1
export GIN_MODE=release

11.2 编译二进制文件

Go 可以编译成单个二进制文件,非常方便部署:

1
2
# 编译
go build -o myapp main.go

编译完成后,你会得到一个 myapp 文件,这就是你的整个应用,不需要任何依赖,直接运行就可以了。

11.3 后台运行

在 Linux 服务器上,你可以用 systemd 来管理你的服务,让它后台运行,开机自启。

创建 /etc/systemd/system/myapp.service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Unit]
Description=My Gin App
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/my-gin-app
ExecStart=/root/my-gin-app/myapp
Restart=always
Environment=GIN_MODE=release

[Install]
WantedBy=multi-user.target

然后:

1
2
3
4
5
6
7
8
# 重载配置
systemctl daemon-reload

# 启动服务
systemctl start myapp

# 设置开机自启
systemctl enable myapp

第十二章:常见问题排查

1. go get 下载依赖超时

  • 解决:设置 GOPROXY 代理,前面已经讲过了。

2. 端口被占用

  • 错误:listen tcp :8080: bind: address already in use

  • 解决:换个端口,或者杀掉占用端口的进程:lsof -i :8080 然后 kill -9 PID

3. 跨域问题

  • 解决:使用 CORS 中间件,前面已经讲过。

4. 参数绑定失败

  • 检查:JSON 格式是否正确,结构体的字段是否是大写(Go 的反射只能访问大写字段),tag 是否正确。

5. 静态文件访问不到

  • 检查:静态文件的路径是否正确,r.Static 的第一个参数是 URL 路径,第二个是本地路径。

总结

恭喜你!读到这里,你已经掌握了 Gin 框架的核心用法:

✅ 环境搭建:Go 安装、Gin 安装、依赖管理
✅ 基础入门:第一个 Hello World 程序
✅ 路由管理:各种 HTTP 方法、参数、路由分组
✅ 请求处理:表单、JSON、文件上传、参数校验
✅ 响应处理:JSON、HTML、静态文件、重定向
✅ 中间件:自定义中间件、CORS、JWT 认证
✅ 配置管理:用 Viper 管理配置文件
✅ 项目实战:完整的用户管理 API
✅ 部署运维:生产环境配置、部署

Gin 是一个非常强大而简洁的框架,它把 Go 语言的高性能和 Web 开发的便捷性完美结合在了一起。希望这篇教程能帮助你快速入门,开启你的 Go Web 开发之旅!

接下来,你可以继续深入学习:

  • 数据库集成(GORM)

  • 限流、熔断等高级中间件

  • 日志系统

  • 监控与链路追踪

  • 容器化部署(Docker)

祝你学习愉快!🚀