21 go语言(golang) - gin框架安装及使用(二)

四、组成

前面的文章中,我们介绍了其中一部分组成,接下来继续学习:

  1. Router(路由器)

    • Gin 使用基于树结构的路由机制来处理 HTTP 请求。它支持动态路由参数、分组路由以及中间件。
    • 路由器负责将请求路径映射到相应的处理函数。
  2. Context(上下文)

    • gin.Context 是 Gin 中最重要的结构之一,它在请求生命周期内传递信息。
    • Context 提供了对请求和响应对象的访问,以及用于存储数据、设置状态码、返回 JSON 等方法。
  3. Middleware(中间件)

    • 中间件是可以在请求被最终处理之前或之后执行的一段代码,用于实现日志记录、错误恢复、认证等功能。
    • Gin 支持全局中间件和特定路由组或单个路由使用的中间件。
  4. Handlers(处理函数)

    • 处理函数是实际执行业务逻辑的位置,每个路由都会关联一个或多个处理函数。
    • 这些函数接收 gin.Context 参数,通过它们可以获取请求数据并生成响应。
  5. Error Handling(错误处理)

    • Gin 提供了一种机制来捕获和管理应用程序中的错误,可以通过 Context 的方法进行错误报告和恢复操作。
  6. Rendering and Responses(渲染与响应)

    • 支持多种格式的数据输出,包括 JSON、XML 和 HTML 渲染等,方便客户端消费不同类型的数据格式。
  7. Binding and Validation(绑定与验证)

    • 自动将 HTTP 请求中的数据绑定到结构体,并支持对输入数据进行验证,以确保其符合预期格式和规则。
  8. Templates (模板)

    • 虽然不是框架核心,但 Gin 支持集成 HTML 模板引擎,用于生成动态网页内容。

4.3 Middleware(中间件)

4.3.4 中间件的调用顺序

上一章讲的中间件,还有部分内容我们先来看看

4.3.4.1 按注册顺序执行

中间件会按照它们被添加到 Gin 实例上的顺序依次执行

go 复制代码
func Test1(t *testing.T) {
	r := gin.Default()

	r.Use(func(c *gin.Context) {
		fmt.Println("func1 ...")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func2 ...")
	})

	r.GET("/test1", func(c *gin.Context) {
		fmt.Println("test1 最终路由方法。。。")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func3 ...")
	})

	r.GET("/test2", func(c *gin.Context) {
		fmt.Println("test2 最终路由方法。。。")
	})

	r.Run()
}

依次调用test1和test2路由,输出

复制代码
func1 ...
func2 ...
test1 最终路由方法。。。
[GIN] 2024/12/13 - 15:11:44 | 200 |      24.436µs |             ::1 | GET      "/test1"
func1 ...
func2 ...
func3 ...
test2 最终路由方法。。。
[GIN] 2024/12/13 - 15:11:48 | 200 |      21.672µs |             ::1 | GET      "/test2"
4.3.4.2 c.Next()控制顺序

c.Next() 是 Gin 框架中 *gin.Context 类型的方法,用于控制中间件链的执行流程。

  • 当一个中间件调用 c.Next() 时,它将暂停当前处理中函数的执行,并将控制权交给下一个处理中函数或最终的路由处理器。
  • 多个中间件通过调用 c.Next() 可以形成嵌套结构,类似于栈。当所有后续步骤完成后,程序会回到先前上下文继续执行剩余代码。即返回时则是相反顺序(即"先进后出")。

可以理解为,将原本顺序执行的后续中间件,嵌套在c.Next()中先执行

go 复制代码
func Test2(t *testing.T) {
	r := gin.Default()

	r.Use(func(c *gin.Context) {
		fmt.Println("func1 begin...")
		//c.Next()
		fmt.Println("func1 end...")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func2 begin...")
		//c.Next()
		fmt.Println("func2 end...")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func3 begin...")
		//c.Next()
		fmt.Println("func3 end...")
	})

	r.GET("/test_next", func(c *gin.Context) {
		fmt.Println("test_next 最终路由方法。。。")
	})

	r.Run()
}

当注释掉所有c.Next()时,顺序执行

复制代码
func1 begin...
func1 end...
func2 begin...
func2 end...
func3 begin...
func3 end...
test_next 最终路由方法。。。

打开func1的c.Next()时,后续的中间件和最终路由就全部在c.Next()中执行完,最后才返回func1继续执行,即最后再打印func1 end...

复制代码
func1 begin...
func2 begin...
func2 end...
func3 begin...
func3 end...
test_next 最终路由方法。。。
func1 end...

再打开func2的c.Next()时,道理相同,c.Next()中嵌套c.Next()

go 复制代码
func1 begin...
func2 begin...
func3 begin...
func3 end...
test_next 最终路由方法。。。
func2 end...
func1 end...

再打开func3的c.Next()时

复制代码
func1 begin...
func2 begin...
func3 begin...
test_next 最终路由方法。。。
func3 end...
func2 end...
func1 end...
4.3.4.3 c.Abort()控制顺序

c.Abort() 是 Gin 框架中 *gin.Context 类型的方法,用于立即停止当前请求的进一步处理。

  • 一旦调用 c.Abort(),当前请求将不再继续传递给下一个处理中函数或最终的路由处理器。
  • 这意味着所有在调用点之后注册的中间件和路由处理器都不会被执行。
  • 调用 c.Abort() 会设置一个内部标志位,指示该请求已经被终止。这个标志可以通过 c.IsAborted() 方法检查。
  • 在某些情况下,例如认证失败、权限不足或其他需要立即返回响应的情况,可以使用 c.Abort() 来阻止进一步操作,并直接返回适当的响应给客户端。
go 复制代码
func Test3(t *testing.T) {
	r := gin.Default()

	r.Use(func(c *gin.Context) {
		fmt.Println("func1 begin...")
		fmt.Println("func1 end...")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func2 begin...")
		fmt.Println(c.IsAborted())
		c.Abort() // 从此中间件开始,后续的中间件包括最终路由都不再执行
		fmt.Println(c.IsAborted())
		fmt.Println("func2 end...") // 但是这行代码会执行
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func3 begin...")
		fmt.Println("func3 end...")
	})

	r.GET("/test_next", func(c *gin.Context) {
		fmt.Println("test_next 最终路由方法。。。")
		c.JSON(200, gin.H{
			"success": "true",
		})
	})

	r.Run()
}

输出:

复制代码
func1 begin...
func1 end...
func2 begin...
false
true
func2 end...
4.3.4.3 Next 和 Abort 共同控制?

注:如果Next()中有Abort(),执行顺序是怎样的?

go 复制代码
func Test4(t *testing.T) {
	r := gin.Default()

	r.Use(func(c *gin.Context) {
		fmt.Println("func1 begin...")
		c.Next()
		fmt.Println("func1 end...")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func2 begin...")
		c.Abort()
		fmt.Println("func2 end...")
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func3 begin...")
		fmt.Println("func3 end...")
	})

	r.GET("/test_next", func(c *gin.Context) {
		fmt.Println("test_next 最终路由方法。。。")
		c.JSON(200, gin.H{
			"success": "true",
		})
	})

	r.Run()
}

输出:

复制代码
func1 begin...
func2 begin...
func2 end...
func1 end...

可以看到,c.Next()还是会执行完,并返回后,打印func1 end...c.Abort()只是控制切断了func3的执行,这里很坑,问了很多AI,回答的都是func1 end...不会执行,要自己试过才知道

4.4 Handlers(处理函数)

处理函数(Handlers)是处理 HTTP 请求的核心组件。它们负责接收请求、执行业务逻辑,并返回响应。

4.4.1 Handlers 的基本概念

  1. HandlerFunc 类型

    • 在 Gin 中,所有的处理函数都必须符合 gin.HandlerFunc 类型。
    • gin.HandlerFunc 被定义为:type HandlerFunc func(*Context)
    • 这意味着任何接受一个指向 *gin.Context 参数且无返回值的函数都可以作为一个合法的 Handler。
  2. Context 对象

    • 每个 Handler 都会接收到一个 *gin.Context 对象,它封装了请求和响应的信息。
    • 开发者可以通过这个对象来获取请求数据(如查询参数、表单数据、JSON 数据等)、设置响应状态码和内容,以及控制请求流转(如调用 Next()Abort())。

4.4.2 使用

我们在前面的代码例子中,无论是绑定在路由上,还是作为中间件使用,都是这个HandlerFunc类型

  1. Handlers 通常与特定路由绑定在一起,通过 HTTP 方法(如 GET, POST)以及路径进行匹配。
  2. 也可以多个 Handlers 可以组成中间件链,每个处理中步骤按顺序执行。
  3. 匿名函数或命名函数皆可用作 Handler
go 复制代码
func myHandler(c *gin.Context) {
	fmt.Println("命名函数")
}

func Test5(t *testing.T) {
	r := gin.Default()

	// 使用匿名函数
	my := func(c *gin.Context) {
		fmt.Println("匿名函数1")
	}
	r.GET("/t1", my)

	// 使用匿名函数
	r.GET("/t2", func(c *gin.Context) {
		fmt.Println("匿名函数2")
	})

	// 使用命名函数
	r.GET("/t3", myHandler)

	r.Run()
}

4.5 Error Handling(错误处理)

错误处理(Error Handling)帮助开发者在请求处理过程中捕获和管理错误。Gin 提供了一些机制来简化错误的记录、传递和响应。

4.5.1 基本概念

Gin 使用 gin.Error 类型来表示错误。它包含了一个 error 接口,以及一些附加信息:元数据(Meta any)和类型标识(Type ErrorType),其中的错误类型用于标识不同种类的错误,例如绑定错误、渲染错误等。可以通过设置不同的类型来对错误进行分类。

go 复制代码
// 结构体的源码:
// Error represents a error's specification.
type Error struct {
	Err  error
	Type ErrorType
	Meta any
}

4.5.2 错误处理机制

4.5.2.1 Context 中的 Errors 字段
  • 每个 *gin.Context 对象都有一个 Errors 字段,这是一个存储所有发生在该请求生命周期内的 gin.Error 切片。
  • 开发者可以通过这个字段访问并操作这些累积起来的错误,并根据需要进行进一步操作(如日志记录或响应生成)。
4.5.2.2 添加错误

在处理中函数中,可以使用 c.Error(err) 方法将新的 error 添加到当前上下文中。

go 复制代码
r.Use(func(c *gin.Context) {
		fmt.Println("func1...")
		c.Error(fmt.Errorf("手动写入错误1"))
	})
4.5.2.3 检索错误

可以通过迭代上下文中的 Errors 来访问所有已记录过得 errors。

go 复制代码
func Test6(t *testing.T) {
	r := gin.Default()

	r.Use(func(c *gin.Context) {
		fmt.Println("func1...")
		c.Error(fmt.Errorf("手动写入错误1"))
	})

	r.Use(func(c *gin.Context) {
		fmt.Println("func2...")
		c.Error(fmt.Errorf("手动写入错误2"))
	})

	r.GET("/error", func(c *gin.Context) {
		errors := c.Errors

		// 遍历所有的错误
		for _, err := range errors {
			fmt.Println(err)
		}

		fmt.Println("正常返回")
	})

	r.Run()
}

输出,其中最后两行的日志是gin自带打印的错误信息

复制代码
func1...
func2...
手动写入错误1
手动写入错误2
正常返回
[GIN] 2024/12/16 - 10:50:56 | 200 |      34.129µs |       127.0.0.1 | GET      "/error"
Error #01: 手动写入错误1
Error #02: 手动写入错误2

4.5.3 自定义全局异常处理中间件

为了统一管理应用程序中的异常情况,通常会创建一个全局异常处理中间件。在这个中间件里,可以遍历每个请求产生过得 errors 并做出相应反应,比如写入日志系统等。

go 复制代码
func myErrorHandler(c *gin.Context) {
	c.Next() // 执行后续处理中步骤

	errors := c.Errors
	// 检查是否有任何errors被注册
	if len(errors) > 0 {
		for i, e := range errors {
			fmt.Println(i, e) // 打印或保存日志信息
		}
		// 返回通用响应给客户端
		c.JSON(500, gin.H{
			"msg": "内部错误",
		})
	}
}

func Test7(t *testing.T) {
	r := gin.Default()
	r.Use(myErrorHandler)

	r.GET("/error", func(c *gin.Context) {
		// 模拟业务执行过程中的错误
		c.Error(fmt.Errorf("手动写入错误"))

		c.JSON(200, gin.H{
			"msg": "成功",
		})
	})

	r.Run()
}

客户端输出,可以看到,返回的有点错乱

复制代码
{
  "msg": "成功"
}{
  "msg": "内部错误"
}

所以,要实现类似spring中全局的错误处理,还需要改进一下

go 复制代码
func myErrorHandler2(c *gin.Context) {
	defer func() {
		errors := c.Errors
		// 在golang中,这些错误属于可以预期的错误,可以简单的打印日志或做对应的业务处理,真正类似java中不可预期的是panic
		if len(errors) > 0 {
			for i, e := range errors {
				fmt.Println(i, e) // 打印或保存日志信息
			}
		}
		// 这里处理真正的不可预期的报错
		hasPanic := recover()
		if hasPanic != nil {
			fmt.Println("捕获到异常!!")

			c.Abort() // 这行代码一定要加,不然后续代码还会执行,比如在最终路由中的打印
			// 返回通用响应给客户端
			c.JSON(500, gin.H{
				"msg": "内部错误",
			})
		}
	}()

	c.Next() // 执行后续处理中步骤
}

func Test8(t *testing.T) {
	r := gin.Default()
	r.Use(myErrorHandler2)
	r.Use(func(c *gin.Context) {
		// 模拟错误
		list := []int{1, 2, 3, 4}
		i := list[5]
		println(i)
	})

	r.GET("/error", func(c *gin.Context) {
		fmt.Println("正常进入最终路由")

		c.JSON(200, gin.H{
			"msg": "成功",
		})
	})

	r.Run()
}

4.5.4 AbortWithError

Gin 提供了便捷方法 AbortWithError(statusCode int , err error ) ,用于同时终止请求并将指定状态码与error对象一起加入到context.errors列表内 。这使得开发者能够快速地结合HTTP协议标准状态码与具体业务逻辑需求来构建更具表达力且一致性强大的API接口 。

go 复制代码
func Test9(t *testing.T) {
	r := gin.Default()
	r.Use(func(c *gin.Context) {
		fmt.Println("func1...")
	})
	r.Use(func(c *gin.Context) {
		random := rand.Intn(2)
		// 模拟随机错误
		if random == 0 {
			// 将错误添加到Context中,并且终止后续的调用链
			c.AbortWithError(500, fmt.Errorf("内部错误"))
			// 等价于
			//c.Error(fmt.Errorf("内部错误"))
			//c.AbortWithStatus(500)

			return
		}
		fmt.Println("func2...")
	})
	r.Use(func(c *gin.Context) {
		fmt.Println("func3...")
	})

	r.GET("/error", func(c *gin.Context) {
		c.JSON(200, "正常")
	})

	r.Run()
}

输出

复制代码
// 异常情况
func1...
[GIN] 2024/12/16 - 16:34:28 | 500 |      18.752µs |       127.0.0.1 | GET      "/error"
Error #01: 内部错误

// 正常情况
func1...
func2...
func3...
[GIN] 2024/12/16 - 16:34:30 | 200 |      61.868µs |       127.0.0.1 | GET      "/error"
相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa9 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio