Golang Gin框架基础与实践指南

Golang Gin框架


在这里插入图片描述

一. Gin框架介绍

Gin 是一个用 Golang编写的 高性能的web 框架, 由于http路由的优化,速度提高了近 40 倍。 Gin的特点就是封装优雅、API友好。

Gin的一些特性:

  • 快速 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
  • 支持中间件 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
  • Crash 处理 Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
  • JSON 验证 Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
  • 路由组 更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
  • 错误管理 Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
  • 内置渲染 Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
  • 可扩展性 新建一个中间件非常简单。

Go世界里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。

  • Gin 的官网:https://gin-gonic.com/zh-cn/
  • Github 地址:https://github.com/gin-gonic/gin

二. Gin 环境搭建

要安装 Gin 软件包,需要先安装 Go 并设置 Go 工作区(demo)
下载并安装 gin

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

如果无法下载,访问不到网络,请求执行下面的命令

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

就可以下载没法下载的包了

环境测试:

创建go文件:

package main

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

func main() {
    //创建一个默认的路由引擎
	r := gin.Default()
    //配置路由
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"username": "name1",
			"data":     "data1",
		})
	})

	r.Run(":81") //开放端口
}

运行测试:

C:\Users\15509>curl -i 192.168.100.10:81
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 25 May 2025 07:35:48 GMT
Content-Length: 35

{"data":"data1","username":"name1"}
C:\Users\15509>

环境搭建完成。

三. Gin框架中RESTfunAPI

RESTful API概述

RESTful API 是目前比较成熟的一套互联网应用程序的 API 设计理论,所以我们设计我们的路由的时候建议参考 RESTful API 指南。

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。

只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。

例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法URL含义
GET/book查询书籍信息
POST/create_book创建书籍记录
POST/update_book更新书籍信息
POST/delete_book删除书籍信息

同样的需求我们按照RESTful API设计如下:

请求方法URL含义
GET/book查询书籍信息
POST/book创建书籍记录
PUT/book更新书籍信息
DELETE/book删除书籍信息

Gin框架支持开发RESTful API的开发。

总结:

请求资源说明
GET (SELECT)从服务器取资源
POST(CREATE)新建资源
PUT (UPDATE)更新资源
DELETE删除资源

RESTfulAPI使用

使用代码格式:

package main

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

func main() {
    //创建默认路由引擎
	r := gin.Default()
    
    //GET方法
	r.GET("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "GET",
		})
	})

    //POST方法
	r.POST("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "POST",
		})
	})

    //PUT方法
	r.PUT("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "PUT",
		})
	})

    //DELETE方法
	r.DELETE("/book", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "DELETE",
		})
	})

    //端口设置
	r.Run(":81")
}

使用Postman可以进行测试:

测试后台显示:

[GIN-debug] Listening and serving HTTP on :81
[GIN] 2025/05/25 - 16:10:56 | 200 |       53.42µs |   192.168.100.1 | GET      "/book"
[GIN] 2025/05/25 - 16:10:59 | 200 |       16.18µs |   192.168.100.1 | POST     "/book"
[GIN] 2025/05/25 - 16:11:07 | 200 |      19.967µs |   192.168.100.1 | PUT      "/book"
[GIN] 2025/05/25 - 16:11:10 | 200 |      11.161µs |   192.168.100.1 | DELETE   "/book"

四. Gin 框架格式渲染

HTML渲染

首先定义一个存放模板文件的templates文件夹,然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。 posts/index.html文件的内容如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>

users/index.html文件的内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>users/index</title>
</head>
<body>
    {{.title}}
</body>
</html>

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()
	// 自动加载所有子目录模板(根据自己定义的目录设置相应路径)
	r.LoadHTMLGlob("Gin/templates/**/*")
	r.GET("/posts/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.html", gin.H{
			"title": "posts index",
		})
	})

	r.GET("/users/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.html", gin.H{
			"title": "users index",
		})
	})

	r.Run(":81")
}

使用Postman进行测试,测试后端结果:

[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] Loaded HTML Templates (2): 
	- index.html
	- 

[GIN-debug] GET    /posts/index              --> main.main.func1 (3 handlers)
[GIN-debug] GET    /users/index              --> main.main.func2 (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 :81
[GIN] 2025/05/25 - 18:34:11 | 200 |     293.919µs |   192.168.100.1 | GET      "/posts/index"
[GIN] 2025/05/25 - 18:34:26 | 200 |     143.057µs |   192.168.100.1 | GET      "/users/index"

静态文件处理

当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static方法即可。

func main() {
	r := gin.Default()
	r.Static("/static", "./static")
	r.LoadHTMLGlob("templates/**/*")
   // ...
	r.Run(":8080")
}

JSON渲染

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	//方法一:自己拼接JSON
	//gin.H 是是map[string]interface{}的缩写
	r.GET("/someJSON", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"Message": "helloworld",
		})
	})

	//方法二:使用结构体的形式
	r.GET("/moreJSON", func(c *gin.Context) {
		var msg = struct {
			Name    string `json:"user"`
			Message string
			Age     int
		}{}
		msg.Name = "zhangsan"
		msg.Age = 19
		msg.Message = "helloworld"
		c.JSON(http.StatusOK, msg)
	})

	r.Run(":81")
}

进行测试:

C:\Users\15509>curl -i 192.168.100.10:81/someJSON
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 25 May 2025 10:55:57 GMT
Content-Length: 24

{"Message":"helloworld"}
C:\Users\15509>curl -i 192.168.100.10:81/moreJSON
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 25 May 2025 10:55:59 GMT
Content-Length: 51

{"user":"zhangsan","Message":"helloworld","Age":19}
C:\Users\15509>

后台结果显示:

GOROOT=/usr/local/go #gosetup
GOPATH=/root/go #gosetup
/usr/local/go/bin/go build -o /root/.cache/JetBrains/GoLand2025.1/tmp/GoLand/___go_build_students_Gin students/Gin #gosetup
/root/.cache/JetBrains/GoLand2025.1/tmp/GoLand/___go_build_students_Gin #gosetup
[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    /someJSON                 --> main.main.func1 (3 handlers)
[GIN-debug] GET    /moreJSON                 --> main.main.func2 (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 :81
[GIN] 2025/05/25 - 18:55:57 | 200 |      49.973µs |   192.168.100.1 | GET      "/someJSON"
[GIN] 2025/05/25 - 18:55:59 | 200 |      94.536µs |   192.168.100.1 | GET      "/moreJSON"

XML渲染

注意需要使用具名的结构体类型。

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	//方法一:自己拼接JSON
	//gin.H 是是map[string]interface{}的缩写
	r.GET("/someXML", func(c *gin.Context) {
		c.XML(http.StatusOK, gin.H{
			"Message": "helloworld",
		})
	})

	//方法二:使用结构体的形式
	r.GET("/moreXML", func(c *gin.Context) {
		type MessageRecord struct {
			Name    string
			Message string
			Age     int
		}
		var msg MessageRecord
		msg.Name = "zhangsan"
		msg.Message = "helloworld"
		msg.Age = 20
		c.XML(http.StatusOK, msg)
	})

	r.Run(":81")
}

进行测试:

C:\Users\15509>curl -i 192.168.100.10:81/someXML
HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8
Date: Sun, 25 May 2025 11:02:28 GMT
Content-Length: 40

<map><Message>helloworld</Message></map>
C:\Users\15509>curl -i 192.168.100.10:81/moreXML
HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8
Date: Sun, 25 May 2025 11:02:31 GMT
Content-Length: 94

<MessageRecord><Name>zhangsan</Name><Message>helloworld</Message><Age>20</Age></MessageRecord>
C:\Users\15509>

其他渲染

YMAL渲染

r.GET("/someYAML", func(c *gin.Context) {
	c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
})

protobuf渲染

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

五. Gin框架参数获取

在使用Gin处理HTTP请求时,经常需要从客户端接收各种类型的参数(如查询字符串、表单数据或JSON等)。

以下是继红常用方法:

获取QueryString参数

当你想从URL的查询部分(例如:http://192.168.100.10:81/user/search?username=lisi&address=beijing)获取参数时,可以使用c.Query()方法。

实现代码:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	r.GET("/user/search", func(c *gin.Context) {
		username := c.Query("username")
		//可以添加默认值
		//username := c.DefaultQuery("username", "zhangsan")
		address := c.Query("address")

		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})

	r.Run(":81")
}

输入对应的URL就能获取到对应的参数了。

//浏览器测试:
测试地址:http://192.168.100.10:81/user/search?username=zhangsan&adress=shenyang
测试输出:{"address":"","message":"ok","username":"zhangsan"}

获取PostForm参数

请求的数据通过form表单来提交,例如向/user/search发送一个POST请求,获取请求数据的方式如下:

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	r.POST("/user/search", func(c *gin.Context) {
		// DefaultPostForm取不到值时会返回指定的默认值
		username := c.DefaultPostForm("username", "zhangsan")
		//username := c.Query("username")
		address := c.PostForm("address")

		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})

	r.Run(":81")
}

进行POST测试:

C:\Users\15509>curl -X POST http://192.168.100.10:81/user/search
{"address":"","message":"ok","username":"zhangsan"}
C:\Users\15509>curl -X POST http://192.168.100.10:81/user/search -d "username=hongzhenyao&address=shenyang"
{"address":"shenyang","message":"ok","username":"hongzhenyao"}

参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryStringform表单JSONXML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSONform表单QueryString类型的数据,并把值绑定到指定的结构体对象。

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"net/http"
)

type Login struct {
	User     string `form:"user" json:"user" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

func main() {
	route := gin.Default()
	//绑定JSON的示例,({"user": "q1mi", "password": "123456"})
	route.POST("/loginJSON", func(c *gin.Context) {
		var login Login

		if err := c.ShouldBindJSON(&login); err == nil {
			fmt.Printf("login info :%#v\n", login)
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	//绑定form表单(user=zhangsan&password=123456)
	route.POST("/loginForm", func(c *gin.Context) {
		var login Login
		//明确使用ShouldBindWith指定表单绑定
		if err := c.ShouldBindWith(&login, binding.Form); err == nil {
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
	route.GET("/loginQuery", func(c *gin.Context) {
		var login Login
		if err := c.ShouldBindQuery(&login); err == nil {
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	route.Run(":8081")
}

进行测试:测试JSON绑定 (/loginJSON)

测试1[root@master goproject]# curl -X POST http://localhost:8081/loginJSON \
>   -H "Content-Type: application/json" \
>   -d '{"user":"zhangsan","password":"123456"}'
{"password":"123456","user":"zhangsan"}
测试点:
如果缺少字段会返回验证错误,例如尝试去掉password字段:
[root@master goproject]# curl -X POST http://localhost:8081/loginJSON -H "Content-Type: application/json" -d '{"user":"zhangsan"}'
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}

测试表单绑定 (/loginForm)

[root@master goproject]# curl -X POST http://localhost:8081/loginForm \
>   -d "user=zhangsan&password=123456" \
>   -H "Content-Type: application/x-www-form-urlencoded"
{"password":"123456","user":"zhangsan"}

特殊测试案例:
测试multipart表单上传(会自动处理):
[root@master goproject]# curl -X POST http://localhost:8081/loginForm \
>   -F "user=wangwu" \
>   -F "password=7890"
{"password":"7890","user":"wangwu"}

测试查询参数绑定 (/loginQuery):

[root@master goproject]# curl -X GET "http://localhost:8081/loginQuery?user=lisi&password=abcd"
{"password":"abcd","user":"lisi"}

六. Gin框架路由详解

路由(Routing)是由一个 URI(或者叫路径)和一个特定的 HTTP 方法(GET、POST 等) 组成的,涉及到应用如何响应客户端对某个网站节点的访问。

GET POST 以及获取 Get Post 传值

GET请求传值

func main() {
	route := gin.Default()
	route.GET("/user", func(c *gin.Context) {
		uid := c.Query("uid")
		page := c.DefaultQuery("page", "0")
		c.String(200, uid, page)
	})
	route.Run(":8081")
}

测试:/user?uid=10086&page=20

[root@master goproject]# curl -i http://localhost:8081/user?uid=10086&page=20
[1] 41757
[root@master goproject]# HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Mon, 26 May 2025 01:26:24 GMT
Content-Length: 23

10086%!(EXTRA string=0)

GET动态路由传值

func main() {
	route := gin.Default()
	route.GET("/user/:id", func(c *gin.Context) {
		id := c.Param("id")
		c.JSON(200, gin.H{
			"news": id,
		})
	})

	route.Run(":8081")
}

测试:/user/20

[root@master goproject]# curl -i http://localhost:8081/user/250
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 26 May 2025 01:34:53 GMT
Content-Length: 14

{"news":"250"}

Post 请求传值 获取 form 表单数据

  • 通过 c.PostForm 接收表单传过来的数据
  • postman进行数据提交
package main

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

func main() {
	route := gin.Default()
	route.POST("/doAddUser", func(c *gin.Context) {
		username := c.PostForm("username")
		password := c.PostForm("password")
		age := c.DefaultPostForm("age", "20")

		c.JSON(200, gin.H{"username": username, "password": password, "Age": age})
	})

	route.Run(":8081")
}

进行测试:

[root@master goproject]# curl -X POST http://localhost:8081/doAddUser \
>   -d "username=admin" \
>   -d "password=123456" \
>   -H "Content-Type: application/x-www-form-urlencoded"
{"Age":"20","password":"123456","username":"admin"}
[root@master goproject]# 
[root@master goproject]# curl -X POST http://localhost:8081/doAddUser \
>   -d "username=test" \
>   -d "password=654321" \
>   -d "age=25" \
>   -H "Content-Type: application/x-www-form-urlencoded"
{"Age":"25","password":"654321","username":"test"}

Get 传值绑定到结构体

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// 定义一个结构体
type Userlogin struct {
	Username string `form:"username" json:"username"`
	Password string `form:"password" json:"password"`
}

func main() {
	route := gin.Default()
	route.GET("/", func(c *gin.Context) {
		var userlogin Userlogin
        //请求类型判断阶段,Gin 会通过 Content-Type 自动选择绑定器
	    //将请求参数绑定到 userlogin 结构体
		if err := c.ShouldBind(&userlogin); err == nil {
			c.JSON(http.StatusOK, userlogin)
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	route.Run(":8081")
}

测试:/?username=zhangsan&password=123456

测试地址:http://192.168.100.10:8081/?username=zhangsan&password=123456
测试结果:{"username":"zhangsan","password":"123456"}

POST传值到结构体

	route.POST("doLogin", func(c *gin.Context) {
		var userpostlogin Userlogin
		if err := c.ShouldBind(&userpostlogin); err == nil {
			c.JSON(http.StatusOK, userpostlogin)
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

测试:/doLogin

测试地址:http://192.168.100.10:8081/doLogin
测试参数:username:admin password:123456
测试结果:{"username":"admin","password":"123456"}

获取 Post Xml 数据

通过ctx.ShouldBindXML获取xml中的数据

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// 定义一个结构体
type Article struct {
	Title   string `json:"title" xml:"title"`
	Content string `json:"content" xml:"content"`
}

func main() {
	route := gin.Default()
	route.POST("/xml", func(ctx *gin.Context) {
		var article Article
		if err := ctx.ShouldBindXML(&article); err == nil {
			ctx.JSON(http.StatusOK, article)
		} else {
			ctx.JSON(http.StatusBadRequest, gin.H{
				"err": err.Error(),
			})
		}
	})
	route.Run(":8081")
}

测试结果:/xml

请求体:
<?xml version="1.0" encoding="UTF-8"?>
<article>
	<content type="string">I am zhangsan</content>
	<title type="string">zhangsan</title>
</article>

请求结果:
{"title":"zhangsan","content":"I am zhangsan"}

路由分组

路由分组的概念

Gin的路由分组基于路径前缀继承机制,通过Group()方法创建的子路由组会自动继承父级路径前缀。当注册路由时,系统会将组前缀与具体路由路径进行字符串拼接,形成完整访问路径。例如group := router.Group("/api")创建基础路径,组内路由group.GET("/users")的实际路径为/api/users

分组的核心价值在于共享配置,同一组内的路由可共用中间件、路径前缀等设置。路径拼接时需注意:若组路径以/结尾或子路径以/开头,会自动合并重复斜杠(如/api//v1会规范为/api/v1),但若路径片段无斜杠连接(如group.GET("users")),会直接拼接成/apiusers,需显式用/分隔层级。

package main

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

func main() {
	route := gin.Default()
	userGroup := route.Group("user")
	{
		// 访问路径: /user/index
		userGroup.GET("/index", func(c *gin.Context) {})
		// 访问路径: /user/login
		userGroup.GET("/login", func(c *gin.Context) {})
		// 访问路径: /user/login
		userGroup.POST("/login", func(c *gin.Context) {})
	}
	shopGroup := route.Group("/shop")
	{
		// 访问路径: /shop/index
		shopGroup.GET("/index", func(c *gin.Context) {})
		// 访问路径: /shop/cart
		shopGroup.GET("/cart", func(c *gin.Context) {})
		// 访问路径: /shop/hello
		shopGroup.POST("/hello", func(c *gin.Context) {})
	}
	route.Run(":8081")
}

路由分离

路由分离是指将不同业务模块的路由配置拆分成独立文件的管理方式

在项目新建文件夹router,然后在router目录下创建apiRouter.goadminRouter.go,内容如下:

package route

//apiRoute.go

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

func ApiRoute(r *gin.Engine) {
	apiRoute := r.Group("/api")
	{
		apiRoute.GET("/", func(ctx *gin.Context) {
			ctx.String(200, "api")
		})

		apiRoute.GET("articles", func(ctx *gin.Context) {
			ctx.String(200, "/api/articles")
		})
	}
}
package route

//adminRoute.go

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func AdminRoute(r *gin.Engine) {
	adminRoute := r.Group("/admin")
	{
		adminRoute.GET("/", func(ctx *gin.Context) {
			ctx.String(200, "admin")
		})

		adminRoute.GET("list", func(ctx *gin.Context) {
			ctx.String(http.StatusOK, "admin/list")
		})
	}
}

main.go中引入路由模块:

package main

import (
	"github.com/gin-gonic/gin"
	"students/Gin/route"
)

func main() {
	r := gin.Default()
	route.ApiRoute(r)
	route.AdminRoute(r)
	r.Run(":8081")
}

进行测试:

C:\Users\15509>curl  192.168.100.10:8081/api/
api
C:\Users\15509>curl  192.168.100.10:8081/api/articles
/api/articles

C:\Users\15509>curl  192.168.100.10:8081/admin/
admin
C:\Users\15509>curl  192.168.100.10:8081/admin/list
admin/list

七. Gin框架中的自定义控制器

控制器分组

当我们的项目比较大的时候有必要对我们的控制器进行分组 , 业务逻辑放在控制器中.
在项目文件夹下面新建controller\api\文件夹,创建userController.go
userController.go 内容:

package api

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

func UserIndex(c *gin.Context) {
	c.String(200, "api UserIndex")
}

func UserAdd(c *gin.Context)  {
	c.String(200, "api UserAdd")
}

func UserList(c *gin.Context) {
	c.String(200, "api UserList")
}

func UserDelete(c *gin.Context) {
	c.String(200, "api UserDelete")
}
  • apiRouter.go中调用userController.go 的函数
import (
	"gin/controller/api"

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

func ApiRouter(r *gin.Engine) {
	apiRouter := r.Group("/api")
	{
		apiRouter.GET("/", )

		apiRouter.GET("users", api.UserIndex)
		apiRouter.GET("users/:id", api.UserList)
		apiRouter.POST("users", api.UserAdd)
		apiRouter.DELETE("users/:id", api.UserDelete)
	}
}
  • 为了使方法能够继承,我们修改userController.go 内容
package api

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

//结构体封装控制器
type UserController struct {}

func (con UserController) Index(c *gin.Context) {
	c.String(200, "api Index")
}

func (con UserController) Add(c *gin.Context)  {
	c.String(200, "api Add")
}
 
func (con UserController) List(c *gin.Context) {
	c.String(200, "api List")
}

func (con UserController) Update(c *gin.Context) {
	c.String(200, "api update")
}

func (con UserController) Delete(c *gin.Context) {
	c.String(200, "api Delete")
}
  • 结构体作用:将同一业务模块的处理方法组织在一起,实现逻辑聚合
  • 方法接收器(con UserController) 表明这些方法属于该结构体,虽未使用实例变量,但为后续功能扩展预留空间

完善apiRouter.go

func ApiRouter(r *gin.Engine) {
	apiRouter := r.Group("/api")
	{
		apiRouter.GET("/")

		// api.UserController{} 实例化后才可以使用结构体的方法
		apiRouter.GET("users", api.UserController{}.List)
		apiRouter.GET("users/:id", api.UserController{}.List)
		apiRouter.POST("users", api.UserController{}.Add)
		apiRouter.DELETE("users/:id", api.UserController{}.Delete)
	}
}

  • 实例化过程UserController{} 创建结构体实例,通过实例调用方法
  • 方法绑定:Gin 路由直接接收结构体方法作为处理函数

控制器的继承

继承后就可以调用控制器里面的公共方法:

  • api目录下新建baseController.go,内容如下:
package api

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

type BaseController struct{}

func (con BaseController) success(c *gin.Context) {
	c.String(200, "success")
}

func (con BaseController) error(c *gin.Context) {
	c.String(200, "failed")
}

修改userController.go:

package api

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

// 继承BaseController
// 嵌入结构体实现组合
type UserController struct {
	BaseController
}

func (con UserController) Index(c *gin.Context) {
	//c.String(200, "api UserIndex")
	con.success(c)
    // 调用基类方法
}

func (con UserController) Add(c *gin.Context) {
	c.String(200, "api UserAdd")
}

func (con UserController) List(c *gin.Context) {
	c.String(200, "api UserList")
}

func (con UserController) Delete(c *gin.Context) {
	c.String(200, "api UserDelete")
}

通过这种控制器组织方式,可以构建出高可维护性的 Gin 项目结构。核心思想是通过 Go 语言的组合特性实现代码复用,同时利用路由组的层级关系管理不同业务模块的访问路径。

八. Gin框架中间件

Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、 记录日志、耗时统计等操作。通俗的讲:中间件就是匹配路由前和匹配路由完成后执行的一系列操作

路由中间件

Gin中的中间件必须是一个gin.HandlerFunc类型,配置路由的时候可以传递多个 func 回调函数。中间件要放在最后一个回调函数的前面 ,触发的方法都可以称为中间件。

//中间件函数
func InitMiddleWare(c *gin.Context) {
	fmt.Println("init middle ware")
}

func ApiRoute(r *gin.Engine) {
	apiRoute := r.Group("/api")
	{
        // 中间件要放在最后一个回调函数的前面
		apiRoute.GET("/",InitMiddleWare,api.UserController{}.Index)
	}
}

ctx.Next()调用该请求的剩余处理程序

中间件里面加上 ctx.Next()后,c.Next()的语句后面先不执行,跳转到后面的中间件和回调函数中执行完后,才执行c.Next()后面的语句可以让我们在路由匹配完成后执行一些操作。比如我们统计一个请求的执行时间:

func InitMiddleWare(c *gin.Context) {
	fmt.Println("1 - init middle ware")
	start := time.Now().UnixNano()
	// 调用c.Next()请求的剩余处理程序
	// c.Next()的语句后面先不执行,先跳转路由匹配的最后一个回调函数执行后,
	// 才执行c.Next()后面的语句
	//说白了,就是等最后一个函数执行完在执行c.Next后面的
	c.Next()
	fmt.Println("3-程序执行完成.计算时间")
	end := time.Now().UnixNano()

	//  计算耗时,Go 语言中的 Since()函数保留时间值,并用于评估与实际时间的差异
	fmt.Println(end - start)
}

func ApiRoute(r *gin.Engine) {
	apiRoute := r.Group("/api")
	{
		apiRoute.GET("/", InitMiddleWare, func(ctx *gin.Context) {
			fmt.Println("2 - 中间件")
			ctx.String(200, "/api")
		})
	}
}

测试结果:

1 - init middle ware
3-程序执行完成.计算时间
1603

个路由配置多个中间件的执行顺序,多个中间件执行顺序:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func InitMiddleWareOne(c *gin.Context) {
	fmt.Println("one - init middleware start")
	c.Next()
	fmt.Println("one - init middleware end")
}

func InitMiddleWareTwo(c *gin.Context) {
	fmt.Println("Two - init middleware start")
	c.Next()
	fmt.Println("Two - init middleware end")
}

func ApiRoute(r *gin.Engine) {
	apiRoute := r.Group("/api")
	{
        //One先开始,然后Two开始
        //Two后面的函数执行完毕,执行Two
        //One后面的Two执行完毕,执行One
		apiRoute.GET("/", InitMiddleWareOne, InitMiddleWareTwo, func(ctx *gin.Context) {
			fmt.Println("2 - 中间件")
			ctx.String(200, "/api")
		})
	}
}

func main() {
	r := gin.Default()
	ApiRoute(r)
	r.Run(":8081")
}

测试结果:

[GIN-debug] redirecting request 301: /api/ --> /api/
one - init middleware start
Two - init middleware start
2 - 中间件
Two - init middleware end
one - init middleware end

Abort是终止的意思,ctx.Abort()表示终止调用该请求的剩余处理程序,Abort()后,中间件后面的回调函数(包括后面的中间件)不执行了,直接执行该中间件这里面的语句

九. 全局中间件

设置方法:

  • r.Use(中间件1,中间件2…)
func InitMiddleWareOne(c *gin.Context) {
	fmt.Println("one - init middleware start")
	c.Next()
	fmt.Println("one - init middleware end")
}

func InitMiddleWareTwo(c *gin.Context) {
	fmt.Println("Two - init middleware start")
	c.Next()
	fmt.Println("Two - init middleware end")
}

func main() {
	r := gin.Default()
	//全局中间件配置
	r.Use(InitMiddleWareOne)
	r.Use(InitMiddleWareTwo)
	
	ApiRoute(r)
	r.Run(":8081")
}

路由单独注册

// 给/test2路由单独注册中间件(可注册多个)
	r.GET("/test2", StatCost(), func(c *gin.Context) {
		name := c.MustGet("name").(string) // 从上下文取值
		log.Println(name)
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello world!",
		})
	})

为路由组注册中间件

为路由组注册中间件有以下两种写法。

写法1:字段:StatCost()

shopGroup := r.Group("/shop", StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

写法2:

shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

中间件注意事项

gin默认中间件

gin.Default()默认使用了LoggerRecovery中间件,其中:

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release
  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

Gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

func LoginMiddleWare(c *gin.Context) {
	fmt.Println("login middle ware")
	c.Set("username", "张三")

	// 定义一个goroutine统计日志
	cCp := c.Copy()
	go func() {
		time.Sleep(2 * time.Second)
		fmt.Println("Done in path " + cCp.Request.URL.Path)
	}()
}

func main() {
	r := gin.Default()
	r.Use(LoginMiddleWare)
	r.Run(":8081")
}

测试结果:

[GIN-debug] Listening and serving HTTP on :8081
login middle ware
[GIN] 2025/05/26 - 15:28:41 | 404 |      22.382µs |   192.168.100.1 | GET      "/"
Done in path /
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值