gin web框架
1 gin框架安装
GitHub地址:https://github.com/gin-gonic/gin
-
安装
go get -u github.com/gin-gonic/gin
-
第一个Gin示例:
package main import ( "github.com/gin-gonic/gin" ) func main() { // 创建一个默认的路由引擎 r := gin.Default() // GET:请求方式;/hello:请求的路径 // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数 r.GET("/hello", func(c *gin.Context) { // c.JSON:返回JSON格式的数据 c.JSON(200, gin.H{ "message": "Hello world!", }) }) // 启动HTTP服务,默认在0.0.0.0:8080启动服务 r.Run() }
将上面的代码保存并编译执行,然后使用浏览器打开
127.0.0.1:8080/hello
就能看到一串JSON字符串。 -
或者先写好代码,然后在终端中运行
go mod tidy
会自动下载所需依赖,并添加到go.mod中
2 gin hello world
-
第一个Gin示例:
package main import ( "github.com/gin-gonic/gin" ) func main() { // 创建一个默认的路由引擎 router := gin.Default() // GET:请求方式;/hello:请求的路径 // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数 router.GET("/hello", func(c *gin.Context) { // c.JSON:返回JSON格式的数据 c.JSON(200, gin.H{ "message": "Hello world!", }) }) // 启动HTTP服务,默认在0.0.0.0:8080启动服务 router.Run() }
将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello
就能看到一串JSON字符串。
两种启动方式
// 启动方式一
router.Run("0.0.0.0:8080")
// 启动方式二
http.ListenAndServe("0.0.0.0:8080", router)
3 响应
状态码
状态码是在计算机通信和网络协议中使用的一种标识,用于表示请求的处理结果或错误状态。常见的状态码包括HTTP状态码、TCP/IP状态码和操作系统错误码等。
HTTP状态码是在HTTP协议中使用的一种状态指示,用于表示HTTP请求的处理结果。以下是一些常见的HTTP状态码及其含义:
- 200 OK:请求成功,服务器成功处理了请求。
- 201 Created:请求成功并且服务器创建了新的资源。
- 400 Bad Request:请求无效,服务器无法理解请求的语法或参数。
- 401 Unauthorized:未经授权,请求需要用户身份验证。
- 403 Forbidden:禁止访问,服务器拒绝请求访问资源。
- 404 Not Found:未找到,服务器无法找到请求的资源。
- 500 Internal Server Error:服务器内部错误,服务器遇到意外情况无法完成请求。
返回字符串
router.GET("/txt", func(c *gin.Context) {
c.String(http.StatusOK, "返回txt")
})
返回json
router.GET("/json", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
// 结构体转json
router.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
type Msg struct {
Name string `json:"user"`
Message string
Number int
}
msg := Msg{"fengfeng", "hey", 21}
// 注意 msg.Name 变成了 "user" 字段
// 以下方式都会输出 : {"user": "hanru", "Message": "hey", "Number": 123}
c.JSON(http.StatusOK, msg)
})
返回xml
router.GET("/xml", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{"user": "hanru", "message": "hey", "status": http.StatusOK})
})
返回yaml
router.GET("/yaml", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"user": "hanru", "message": "hey", "status": http.StatusOK})
})
返回html
先要使用 LoadHTMLGlob()
或者LoadHTMLFiles()
方法来加载模板文件
//加载模板
// 加载全局
router.LoadHTMLGlob("gin框架/templates/*") // 路径下包括html文件
// 加载某文件
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//定义路由
router.GET("/tem", func(c *gin.Context) {
//根据完整文件名渲染模板,并传递参数
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Main website",
})
}
在模板中使用这个title,需要使用{{ .title }}
(在html中编写)
不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径。
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})
})
文件响应
// 在golang中,没有相对文件的路径,它只有相对项目的路径
// 第一个参数是网页路径,第二个是文件路径
// 网页请求这个静态目录的前缀, 第二个参数是一个目录,注意,前缀不要重复
router.StaticFS("/static", http.Dir("static/static"))
// 配置单个文件, 网页请求的路由,文件的路径
router.StaticFile("/titian.png", "static/titian.png")
重定向
// 访问/redirect就可以跳转百度
router.GET("/redirect", func(c *gin.Context) {
//支持内部和外部的重定向
c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
})
-
301 Moved Permanently
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。
-
302 Found
请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
4 请求
查询参数 Query
传参方式:
- 客户端访问时传参:url:127.0.0.1:8080/query?user=张三&id=1
- postman中Params填入想传的KEY和VALUE
func _query(c *gin.Context) {
fmt.Println(c.Query("user"))
fmt.Println(c.GetQuery("user")) // 返回(string, bool),可以判断是否接收到参数
fmt.Println(c.QueryArray("user")) // 拿到多个相同的查询参数
fmt.Println(c.DefaultQuery("addr", "四川省")) // 当addr没传参数时,默认为"四川省"
}
func main() {
router := gin.Default()
router.GET("/query", _query)
router.Run()
}
动态参数 Param
func _param(c *gin.Context) {
fmt.Println(c.Param("user_id")) // 12
fmt.Println(c.Param("book_id")) // 12 123
}
router.GET("/param/:user_id/", _param)
router.GET("/param/:user_id/:book_id", _param)
// ?param/12
// ?param/12/123
表单 PostForm
可以接收 multipart/form-data;
和application/x-www-form-urlencoded
func _form(c *gin.Context) {
fmt.Println(c.PostForm("name"))
fmt.Println(c.PostFormArray("name"))
fmt.Println(c.DefaultPostForm("addr", "四川省")) // 如果用户没传,就使用默认值
forms, err := c.MultipartForm() // 接收所有的form参数,包括文件
fmt.Println(forms, err)
}
func main() {
router := gin.Default()
router.POST("/raw", _raw)
router.Run()
}
原始参数 GetRawData
- json
{
"name": "枫枫",
"age": 21
}
- json解析到结构体
func _raw(c *gin.Context) {
body, _ := c.GetRawData()
contentType := c.GetHeader("Content-Type")
switch contentType {
case "application/json":
// json解析到结构体
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
var user User
err := json.Unmarshal(body, &user)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(user)
}
}
func main() {
router := gin.Default()
router.POST("/raw", _raw)
router.Run()
}
四大请求方式
GET
POST
PUT
DELETE
Restful风格指的是网络应用中就是资源定位和资源操作的风格。不是标准也不是协议。
GET:从服务器取出资源(一项或多项)
POST:在服务器新建一个资源
PUT:在服务器更新资源(客户端提供完整资源数据)
PATCH:在服务器更新资源(客户端提供需要修改的资源数据)
DELETE:从服务器删除资源
// 以文字资源为例
// GET /articles 文章列表
// GET /articles/:id 文章详情
// POST /articles 添加文章
// PUT /articles/:id 修改某一篇文章
// DELETE /articles/:id 删除某一篇文章
package main
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/articles", _getList) // 文章列表
r.GET("/articles/:id", _getDetail) // 文章详情
// postman中点击Body->raw->JSON,输入要传递的JSON
r.POST("/articles", _create) // 添加文章
// postman中点击Body->raw->JSON,输入要传递的JSON
r.PUT("/articles/:id", _update) // 编辑文章
r.DELETE("/articles/:id", _delete) // 删除文章
err := r.Run("127.0.0.1:8080")
if err != nil {
fmt.Println("Run failed, err:", err)
return
}
}
type ArticleModel struct {
Title string `json:"title"`
Content string `json:"content"`
}
type Response struct {
Code int `json:"code"` // 状态码
Data any `json:"data"`
Msg string `json:"msg"`
}
// 文章列表页面
func _getList(c *gin.Context) {
articleList := []ArticleModel{
{"Go语言入门", "这篇文章是《Go语言入门》"},
{"python语言入门", "这篇文章是《python语言入门》"},
{"JavaScript语言入门", "这篇文章是《JavaScript语言入门》"},
}
response := Response{0, articleList, "succeed"}
c.JSON(200, response)
}
// 文章详情
func _getDetail(c *gin.Context) {
article := ArticleModel{"Go语言入门", "这篇文章是《Go语言入门》"}
fmt.Println(c.Param("id"))
response := Response{0, article, "succeed"}
c.JSON(200, response)
}
// json解析到结构体
func _bindJson(c *gin.Context, obj any) (err error) {
body, _ := c.GetRawData()
contentType := c.GetHeader("Content-Type")
switch contentType {
case "application/json":
err = json.Unmarshal(body, &obj)
if err != nil {
fmt.Println(err.Error())
return err
}
}
return nil
}
func _create(c *gin.Context) {
var article ArticleModel
err := _bindJson(c, &article)
if err != nil {
fmt.Println("create_bindJson failed, err:", err)
return
}
c.JSON(200, Response{0, article, "succeed"})
}
// 编辑文章
func _update(c *gin.Context) {
// 获取要修改的书籍id
fmt.Println(c.Param("id"))
// 客户端的json解析到结构体
var article ArticleModel
err := _bindJson(c, &article)
if err != nil {
fmt.Println("update_bindJson failed, err:", err)
return
}
// 根据article结构体修改数据库中书籍id对应的书籍数据,这里未编写相应代码
// 响应修改后的书籍信息,并显示状态码和提示信息
c.JSON(200, Response{0, article, "The modification was successful!"})
}
func _delete(c *gin.Context) {
// 获取要删除的书籍id
fmt.Println(c.Param("id"))
// 在数据库中删除对应id的书籍,这里未编写相应代码
// 传一个空map,没有含义
c.JSON(200, Response{0, map[string]string{}, "The deletion was successful!"})
}
请求头
请求头参数获取
GetHeader
,可以大小写不分,且返回切片中的第一个数据
router.GET("/", func(c *gin.Context) {
// 首字母大小写不区分 单词与单词之间用 "-" 连接
// 用于获取一个请求头
fmt.Println(c.GetHeader("User-Agent"))
//fmt.Println(c.GetHeader("user-agent"))
//fmt.Println(c.GetHeader("user-Agent"))
//fmt.Println(c.GetHeader("user-AGent"))
// 拿全部请求头
// Header 是一个普通的 map[string][]string
fmt.Println(c.Request.Header)
// 如果是使用 Get方法或者是 .GetHeader,那么可以不用区分大小写,并且返回第一个value
fmt.Println(c.Request.Header.Get("User-Agent"))
// 如果是用map的取值方式,请注意大小写问题
fmt.Println(c.Request.Header["User-Agent"])
fmt.Println(c.Request.Header["user-agent"])
// 自定义的请求头,用Get方法也是免大小写
fmt.Println(c.Request.Header.Get("Token"))
fmt.Println(c.Request.Header.Get("token"))
c.JSON(200, gin.H{"msg": "成功"})
})
响应头
设置响应头
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/res", func(c *gin.Context) {
c.Header("Token", "hfaskujfheiku")
c.JSON(200, gin.H{"data": "123"})
})
r.Run(":80")
}
5 bind绑定器
gin中的bind可以很方便的将 前端传递 来的数据与 结构体
进行 参数绑定
,以及参数校验
- 参数绑定:在使用这个功能的时候,需要给结构体加上Tag:
json
form
uri
xml
yaml
Must Bind
不用,校验失败会改状态码
Should Bind
可以绑定 json
,query
,param
,yaml
,xml
如果校验不通过会返回错误
ShouldBindJSON
绑定json
- 在postman中Body传json
package main
import "github.com/gin-gonic/gin"
type UserInfo struct {
Name string `json:"name"`
Age int `json:"age"`
Gender string `json:"gender"`
}
func main() {
router := gin.Default()
router.POST("/", func(c *gin.Context) {
var userInfo UserInfo
err := c.ShouldBindJSON(&userInfo)
if err != nil {
c.JSON(200, gin.H{"msg": "你错了"})
return
}
c.JSON(200, userInfo)
})
router.Run(":80")
}
ShouldBindQuery
绑定查询参数
tag对应为form
传参方式:
- 127.0.0.1/query?name=枫枫&age=21&sex=男
- postman中Params填入想传的KEY和VALUE
// ?name=枫枫&age=21&sex=男
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type UserInfo struct {
Name string `json:"name" form:"name"`
Age int `json:"age" form:"age"`
Gender string `json:"gender" form:"sex"`
}
func main() {
router := gin.Default()
router.POST("/query", func(c *gin.Context) {
var userInfo UserInfo
err := c.ShouldBindQuery(&userInfo)
if err != nil {
fmt.Println(err)
c.JSON(200, gin.H{"msg": "你错了"})
return
}
c.JSON(200, userInfo)
})
router.Run(":80")
}
ShouldBindUri(使用频率不高)
绑定动态参数
tag对应为uri
// /uri/fengfeng/21/男
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type UserInfo struct {
Name string `json:"name" form:"name" uri:"name"`
Age int `json:"age" form:"age" uri:"age"`
Sex string `json:"sex" form:"sex" uri:"sex"`
}
func main() {
router := gin.Default()
router.POST("/uri/:name/:age/:sex", func(c *gin.Context) {
var userInfo UserInfo
err := c.ShouldBindUri(&userInfo)
if err != nil {
fmt.Println(err)
c.JSON(200, gin.H{"msg": "你错了"})
return
}
c.JSON(200, userInfo)
})
router.Run(":80")
}
ShouldBind
会根据请求头中的content-type去自动绑定
form-data的参数也用这个,tag用form
默认的tag就是form
- 绑定
form-data
、x-www-form-urlencode
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type UserInfo struct {
Name string `form:"name"`
Age int `form:"age"`
Sex string `form:"sex"`
}
func main() {
router := gin.Default()
router.POST("/form", func(c *gin.Context) {
var userInfo UserInfo
err := c.ShouldBind(&userInfo)
if err != nil {
fmt.Println(err)
c.JSON(200, gin.H{"msg": "你错了"})
return
}
c.JSON(200, userInfo)
})
router.Run(":80")
}
bind绑定器
需要使用参数验证功能,需要加binding tag
常用验证器
// 不能为空,并且不能没有这个字段,“为空校验”
required: 必填字段,如:binding:"required"
// 针对字符串的长度,汉字单个字和英文单个字母长度都是1
// 当写多个条件时,一定要符合格式(字符串内不能多打空格)
// `json:"name" binding:"required,min=4,max=14"`
min 最小长度,如:binding:"min=5"
max 最大长度,如:binding:"max=10"
len 长度,如:binding:"len=6"
// 针对数字的大小
eq 等于,如:binding:"eq=3"
ne 不等于,如:binding:"ne=12"
gt 大于,如:binding:"gt=10"
gte 大于等于,如:binding:"gte=10"
lt 小于,如:binding:"lt=10"
lte 小于等于,如:binding:"lte=10"
// 针对同级字段的
eqfield 等于其他字段的值,如:PassWord string `binding:"eqfield=Password"`
nefield 不等于其他字段的值
- 忽略字段,如:binding:"-"
示例:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type SignUserMSG struct {
Name string `json:"name" binding:"required,min=4,max=14"`
Age int `json:"age" binding:"required,gte=0,lte=120"`
Password string `json:"password" binding:"required,min=6,max=18"`
RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
}
func main() {
router := gin.Default()
router.POST("/", func(c *gin.Context) {
var user SignUserMSG
err := c.ShouldBindJSON(&user)
if err != nil {
c.JSON(200, gin.H{"msg": err.Error()})
return
}
c.JSON(200, gin.H{"data": user})
})
err := router.Run(":80")
if err != nil {
fmt.Println("run failed, err:", err)
}
}
gin内置验证器
// 枚举 只能是red 或green
oneof=red green
// 字符串
contains=fengfeng // 包含fengfeng的字符串
excludes // 不包含
startswith // 字符串前缀
endswith // 字符串后缀
// 数组
dive // dive后面的验证就是针对数组中的每一个元素
// HobbyList []string `json:"hobbyList" binding:"required,dive,startswith=喜欢"`
// 客户端json中的写法:"hobbyList": ["喜欢x", "喜欢xx"]
// 网络验证
// IP string `json:"ip" binding:"ip"`
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。
// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径
// 日期验证 1月2号下午3点4分5秒在2006年
// 客户端传入的日期数据必须按照设定的格式
datetime=2006-01-02
// Date string `json:"date" binding:"datetime=2006-01-02"`
// 客户端json:"date": "2023-09-30"
// 如果这样则不通过:"date": "2023-09-30 17:05"
自定义错误信息(稍后再看···)
当验证不通过时,会给出错误的信息,但是原始的错误信息不太友好,不利于用户查看
// 当出现错误时,就可以来获取出错字段上的msg。
// err:这个参数为ShouldBindJSON返回的错误信息
// obj:这个参数为绑定的结构体
// 还有一点要注意的是,validator这个包要引用v10这个版本的,否则会出错
type UserInfo struct {
Username string `json:"username" binding:"required" msg:"用户名不能为空"`
Password string `json:"password" binding:"min=3,max=6" msg:"密码长度不能小于3大于6"`
Email string `json:"email" binding:"email" msg:"邮箱地址格式不正确"`
}
// GetValidMsg 返回结构体中的msg参数
func GetValidMsg(err error, obj any) string {
// 使用的时候,需要传obj的指针
getObj := reflect.TypeOf(obj)
// 将err接口断言为具体类型
if errs, ok := err.(validator.ValidationErrors); ok {
// 断言成功
for _, e := range errs {
// 循环每一个错误信息
// 根据报错字段名,获取结构体的具体字段
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
return msg
}
}
}
return err.Error()
}
func main() {
router := gin.Default()
router.POST("/", func(c *gin.Context) {
var user UserInfo
err := c.ShouldBindJSON(&user)
if err != nil {
c.JSON(200, gin.H{"msg": GetValidMsg(err, &user)})
return
}
c.JSON(200, gin.H{"data": user})
})
err := router.Run(":80")
if err != nil {
fmt.Println("run failed, err:", err)
}
}
自定义验证器(稍后再看···)
6 文件上传与下载
单文件上传
-
客户端操作:postman中,Body->form-data,Key选择File,名字填file,Value选上传的文件,然后发送POST请求
-
服务端操作:
package main import ( "fmt" "github.com/gin-gonic/gin" "strconv" ) func main() { router := gin.Default() router.POST("/upload", func(c *gin.Context) { // 上传文件 file, err := c.FormFile("file") if err != nil { c.JSON(200, gin.H{"msg": "上传失败!"}) return } c.JSON(200, gin.H{"msg": "上传成功!", "文件名称": file.Filename, // file.Size单位是字节 "文件大小": strconv.FormatInt(file.Size/1024, 10) + "KB"}) // 保存上传文件 err = c.SaveUploadedFile(file, "./uploads/1.png") if err != nil { fmt.Println("SaveUploadedFile failed!error:", err) return } }) err := router.Run(":80") if err != nil { fmt.Println("run failed, err:", err) } }
服务端保存文件方式
-
SaveUploadedFile
c.SaveUploadedFile(file, dst) // 文件对象 文件路径
-
Create+Copy
file, _ := c.FormFile("file") // 读取文件中的数据,返回文件对象 fileRead, _ := file.Open() dst := "./" + file.Filename // 创建一个文件 out, _ := os.Create(dst) defer out.Close() // 拷贝文件对象到out中 io.Copy(out, fileRead)
读取上传文件
file, _ := c.FormFile("file")
// 读取文件中的数据,返回文件对象
fileRead, _ := file.Open()
data, _ := io.ReadAll(fileRead)
fmt.Println(string(data))
多文件上传
-
客户端操作:postman中,Body->form-data,Key选择File,名字填upload[],Value选上传的多个文件,然后发送POST请求
-
服务端操作:
package main import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func main() { router := gin.Default() router.POST("/uploads", func(c *gin.Context) { // 解析请求中的多部分表单数据,用于获取上传的文件。 form, err := c.MultipartForm() if err != nil { fmt.Println("MultipartForm failed, error:", err) return } // 获取名为 "upload[]" 的表单字段中的文件,注意确保表单字段名与前端发送的一致。 files := form.File["upload[]"] // 保存文件 for i, file := range files { // 将文件保存到 "./uploads/uploadFile_序号.png" 的路径中 err = c.SaveUploadedFile(file, fmt.Sprintf("./uploads/uploadFile_%v.png", i+1)) if err != nil { fmt.Println("SaveUploadedFile failed, error:", err) return } } // 返回成功上传的文件数量 c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) }) err := router.Run(":80") if err != nil { fmt.Println("run failed, err:", err) } }
文件下载
c.File("uploads/12.png")
有些响应,比如图片,浏览器就会显示这个图片,而不是下载,所以我们需要使浏览器唤起下载行为
// 表示是文件流,唤起浏览器下载,一般设置了这个,就要设置文件名
c.Header("Content-Type", "application/octet-stream")
// 用来指定下载下来的文件名
c.Header("Content-Disposition", "attachment; filename="+"牛逼.png")
// 表示传输过程中的编码形式,乱码问题可能就是因为它
c.Header("Content-Transfer-Encoding", "binary")
c.File("uploads/12.png")
注意,文件下载浏览器可能会有缓存,这个要注意一下
解决办法就是加查询参数(后续再说)
前后端分离
-
后端:
// 响应头设置文件名和其他信息 c.Header("fileName", "xxx.png") c.Header("msg", "文件下载成功") c.File("uploads/12.png")
-
前端:(不懂前端,后续再看)
async downloadFile(row) { this.$http({ method: 'post', url: 'file/upload', data:postData, responseType: "blob" }).then(res => { const _res = res.data let blob = new Blob([_res], { type: 'application/png' }); let downloadElement = document.createElement("a"); let href = window.URL.createObjectURL(blob); //创建下载的链接 downloadElement.href = href; downloadElement.download = res.headers["fileName"]; //下载后文件名 document.body.appendChild(downloadElement); downloadElement.click(); //点击下载 document.body.removeChild(downloadElement); //下载完成移除元素 window.URL.revokeObjectURL(href); //释放掉blob对象 })}
7 中间件和路由
-
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等 即比如,如果访问一个网页的话,不管访问什么路径都需要进行登录,此时就需要为所有路径的处理函数进行统一一个中间件
-
Gin中的中间件必须是一个gin.HandlerFunc类型
单独注册中间件
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func indexHandler(c *gin.Context) {
fmt.Println("index.....")
c.JSON(http.StatusOK, gin.H{
"msg": "index",
})
}
//定义一个中间件
func m1(c *gin.Context) {
fmt.Println("m1 in.........")
}
func main() {
r := gin.Default()
//m1处于indexHandler函数的前面,请求来之后,先走m1,再走index
r.GET("/index", m1, indexHandler)
_ = r.Run()
}
多个中间件
router.GET,后面可以跟很多HandlerFunc方法,这些方法其实都可以叫中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func m1(c *gin.Context) {
fmt.Println("m1 ...in")
}
func m2(c *gin.Context) {
fmt.Println("m2 ...in")
}
func main() {
router := gin.Default()
router.GET("/", m1, func(c *gin.Context) {
fmt.Println("index ...")
c.JSON(200, gin.H{"msg": "响应数据"})
}, m2)
router.Run(":8080")
}
/*
m1 ...in
index ...
m2 ...in
*/
中间件拦截响应
c.Abort()拦截,后续的HandlerFunc就不会执行了
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func m1(c *gin.Context) {
fmt.Println("m1 ...in")
c.JSON(200, gin.H{"msg": "第一个中间件拦截了"})
c.Abort()
}
func m2(c *gin.Context) {
fmt.Println("m2 ...in")
}
func main() {
router := gin.Default()
router.GET("/", m1, func(c *gin.Context) {
fmt.Println("index ...")
c.JSON(200, gin.H{"msg": "响应数据"})
}, m2)
router.Run(":8080")
}
中间件放行
-
c.Next(),Next前后形成了其他语言中的请求中间件和响应中间件
-
原理类似栈,Next后的部分入栈,然后按先进后出方式出栈
-
如果其中一个中间件响应了c.Abort(),后续中间件将不再执行,直接按照顺序走完所有的响应中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func m1(c *gin.Context) {
fmt.Println("m1 ...in")
c.Next()
fmt.Println("m1 ...out")
}
func m2(c *gin.Context) {
fmt.Println("m2 ...in")
c.Next()
fmt.Println("m2 ...out")
}
func main() {
router := gin.Default()
router.GET("/", m1, func(c *gin.Context) {
fmt.Println("index ...in")
c.JSON(200, gin.H{"msg": "响应数据"})
c.Next()
fmt.Println("index ...out")
}, m2)
router.Run(":8080")
}
/*
m1 ...in
index ...in
m2 ...in
m2 ...out
index ...out
m1 ...out
*/
全局中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func m10(c *gin.Context) {
fmt.Println("m10 ...in")
c.Next()
fmt.Println("m10 ...out")
}
func m11(c *gin.Context) {
fmt.Println("m11 ...in")
c.Next()
fmt.Println("m11 ...out")
}
func main() {
router := gin.Default()
router.Use(m10, m11)
// 无论请求的是不是下面指定的,只要发生请求,一定会走全局中间件
router.GET("/", func(c *gin.Context) {
fmt.Println("index ...in")
c.JSON(200, gin.H{"msg": "index"})
c.Next()
fmt.Println("index ...out")
})
router.Run(":8080")
}
/*
m10 ...in
m11 ...in
index ...in
index ...out
m11 ...out
m10 ...out
*/
中间件传递数据
-
使用Set设置一个key-value,
-
在后续中间件中使用Get接收数据
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func m10(c *gin.Context) {
fmt.Println("m1 ...in")
c.Set("name", "fengfeng")
}
func main() {
router := gin.Default()
router.Use(m10)
router.GET("/", func(c *gin.Context) {
fmt.Println("index ...in")
name, _ := c.Get("name")
fmt.Println(name)
c.JSON(200, gin.H{"msg": "index"})
})
router.Run(":8080")
}
- value的类型是any类型,所有我们可以用它传任意类型,在接收的时候做好断言即可
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type User struct {
Name string
Age int
}
func m10(c *gin.Context) {
fmt.Println("m1 ...in")
c.Set("name", User{"枫枫", 21})
c.Next()
fmt.Println("m1 ...out")
}
func main() {
router := gin.Default()
router.Use(m10)
router.GET("/", func(c *gin.Context) {
fmt.Println("index ...in")
name, _ := c.Get("name")
// 断言成User类型
user := name.(User)
fmt.Println(user.Name, user.Age)
c.JSON(200, gin.H{"msg": "index"})
})
router.Run(":8080")
}
路由分组
Go语言中的路由分组是一种将一组路由规则归类为一个逻辑单元的功能。它允许您使用相同的中间件和其他配置来组织和管理一组相关的路由规则。使用路由分组,您可以轻松地将一组相关的路由规则从一个文件移动到另一个文件,或者在不同的应用程序中共享路由规则。它还可以让您为一组路由规则定义别名,使其更易于识别和管理。路由分组还可以让您在不同的环境中使用不同的路由规则,从而更好地控制应用程序的行为。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func middle(c *gin.Context) {
fmt.Println("middle ...in")
}
// "middle1()"是一个用于创建中间件函数的函数。
func middle1() gin.HandlerFunc {
// 所以要有这里的返回值
// 这里的代码是程序一开始就会执行
return func(c *gin.Context) {
// 这里是请求来了才会执行
fmt.Println("middle1 ...inin")
}
}
func main() {
router := gin.Default()
// 可以选 middle或 middle1()
// "middle"是一个中间件函数,而"middle1()"是一个调用中间件函数的函数。
// "middle"将在请求经过路由组时被调用,而"middle1()"是一个用于创建中间件函数的函数。
r := router.Group("/api").Use(middle, middle1())
{
r.GET("/index", func(c *gin.Context) {
c.String(200, "index")
})
r.GET("/home", func(c *gin.Context) {
c.String(200, "home")
})
}
router.Run(":8080")
}
gin.Default
- gin.Default()默认使用了Logger和Recovery中间件,其中:
- Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。 Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。 如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
- 使用gin.New,如果不指定日志,那么在控制台中就不会有日志显示
8 日志
gin自带日志
输出日志到log文件
package main
import (
"github.com/gin-gonic/gin"
"io"
"os"
)
func main() {
file, _ := os.Create("gin.log")
// 设置gin的日志输出同时写入到gin.log和标准输出
gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
defer file.Close()
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "/"})
})
router.Run()
}
定义路由格式
启动gin,它会显示所有的路由,默认格式如下
# "main.main.func1" 表示一个名为"func1"的函数,它在"main"包的"main"函数中被调用。
[GIN-debug] POST /foo --> main.main.func1 (3 handlers)
[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
[GIN-debug] GET /status --> main.main.func3 (3 handlers)
gin.DebugPrintRouteFunc = func(
httpMethod,
absolutePath,
handlerName string,
nuHandlers int) {
log.Printf(
"[ feng ] %v %v %v %v\n",
httpMethod,
absolutePath,
handlerName,
nuHandlers,
)
}
/* 输出如下
2022/12/11 14:10:28 [ feng ] GET / main.main.func3 3
2022/12/11 14:10:28 [ feng ] POST /index main.main.func4 3
2022/12/11 14:10:28 [ feng ] PUT /haha main.main.func5 3
2022/12/11 14:10:28 [ feng ] DELETE /home main.main.func6 3
*/
查看路由
router.Routes() // 它会返回已注册的路由列表
环境切换
如果不想看到这些debug日志,那么我们可以改为release模式
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
修改log的显示
默认:
[GIN] 2022/12/11 - 14:22:00 | 200 | 0s | 127.0.0.1 | GET "/"CopyErrorOK!
自定义:
func LoggerWithFormatter(params gin.LogFormatterParams) string {
var statusColor, methodColor, resetColor string
statusColor = params.StatusCodeColor()
methodColor = params.MethodColor()
resetColor = params.ResetColor()
return fmt.Sprintf(
"[ gin ] %s | %s %d %s | \t %s | %s | %s %-7s %s \t %s\n",
// 时间
params.TimeStamp.Format("2006/01/02 - 15:04:05"),
// 状态码,带颜色
statusColor, params.StatusCode, resetColor,
// IP
params.ClientIP,
// 请求耗时
params.Latency,
// 请求方法,带颜色
methodColor, params.Method, resetColor,
// 路径
params.Path,
)
}
func main() {
router := gin.New()
router.Use(gin.LoggerWithFormatter(LoggerWithFormatter))
router.Run()
}