baby sword‘s blog baby sword‘s blog
首页
  • java基础
  • java进阶
大数据
  • mysql

    • mysql索引
    • mysql日志
  • redis

    • 单机下的redis
    • 集群下的redis
  • Spring
  • springboot
  • RPC
  • netty
  • mybatis
  • maven
  • 消息队列
  • kafka
  • zookeeper
  • rocketmq
  • 七大设计原则
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • SpringCloud

    • eureka
  • SpringCloud Alibaba

    • nacos
  • 计算机网络
  • 操作系统
  • 算法
  • 个人项目
  • 个人面试面经
  • 八股记忆
  • 工作积累
  • 逻辑题
  • 面试

    • 百度后端实习二面
GitHub (opens new window)

zhengjian

不敢承担失去的风险,是不可能抓住梦想的
首页
  • java基础
  • java进阶
大数据
  • mysql

    • mysql索引
    • mysql日志
  • redis

    • 单机下的redis
    • 集群下的redis
  • Spring
  • springboot
  • RPC
  • netty
  • mybatis
  • maven
  • 消息队列
  • kafka
  • zookeeper
  • rocketmq
  • 七大设计原则
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • SpringCloud

    • eureka
  • SpringCloud Alibaba

    • nacos
  • 计算机网络
  • 操作系统
  • 算法
  • 个人项目
  • 个人面试面经
  • 八股记忆
  • 工作积累
  • 逻辑题
  • 面试

    • 百度后端实习二面
GitHub (opens new window)
  • 华仔聊技术

  • 业务设计

  • 场景设计

  • 运维

  • 安全

  • 面试

  • mac相关工具推荐

  • 开发工具

  • 人工智能

  • 推荐

  • 阅读

  • 工具

  • 计划

  • 产品

  • 云原生

  • go

    • 基础
    • go项目
    • go channel
    • go list
    • go testing模块
    • go指针
    • go实现LRU
    • go数据结构的内存实现
    • go interfaces
    • go项目分包相关
    • 协程实现原理
    • 垃圾回收与写屏障
    • gin
      • 一.初入gin
        • 1. gin
        • 2. 快速开始案例
      • 二. gin快速入门
        • 1. 响应
        • 如何进行重定向
        • 2.如何获取前端数据
        • 3. 如何设置请求头响应头
        • 请求头相关
        • 响应头相关
        • 4.什么是绑定器
        • must Bind
        • should bind
        • ①shouldBindJSON
        • ②ShouldBindQuery
        • ③ShouldBindUri
        • ④通用shouldbind
        • 5.什么是验证器
        • 6.自定义验证器的错误信息
        • 7. 自定义验证器
        • 8. 文件上传
        • 单文件
        • 服务端保存文件的几种方式
        • SaveUploadedFile
        • Create+Copy
        • 读取上传的文件
        • 多文件上传
        • 9.文件下载
        • 直接响应一个路径下的文件
        • 10. 前后端模式下的文件下载
        • 前端写法
        • 11.gin中间件和路由
        • 单独注册中间件
        • 多个中间件
        • 中间件拦截响应
        • 中间件放行
        • 全局注册中间件
        • 中间件传递数据
        • 路由分组
        • 路由分组注册中间件
        • gin.Default
      • 权限验证
      • 耗时统计
        • 日志
      • 定义路由格式
      • 查看路由
      • 环境切换
      • 修改log的显示
        • 第三方日志框架
    • go中flag的使用
    • go dig
    • linux环境快速搭建go
  • QVM

  • 软件设计师

  • 极客时间

  • 单元测试

  • 其他
  • go
xugaoyi
2023-08-15
目录

gin

# 一.初入gin

# 1. gin

gin是一个web开发框架,类似于java的springMVC

使用前提条件:

  1. go的基础环境

创建项目

image-20230815142150119

点击create即可,最后查看go.mod文件

image-20230815142224003

没有问题

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

下载gin依赖,下载成功后,在看mod,发现会多了gin的依赖

image-20230815143032176

  1. 调试工具postman下载或者apifox

因为一般浏览器无法完成所有的请求功能的调用,所以建议使用一些http调用工具

# 2. 快速开始案例

package main

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

func main() {

	//创建一哥默认的路由
	router := gin.Default()

	//绑定路由规则和函数,即规定访问这个路由的地址,是由哪个函数来处理的(这里的方法也可以定义在外面)
	router.GET("/index", func(context *gin.Context) {
		context.String(200, "hello_world")
	})

	//启动监听,将服务启动发布到本机的所有地址 ifconfig可以看到的所有ip
	router.Run(":8080") //0.0.0.0

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

启动web服务的两种方式

//启动方式一
	//启动监听,将服务启动发布到本机的所有地址 ifconfig可以看到的所有ip
	router.Run(":8080") //0.0.0.0

	//启动方式二
	//原生http方式
	http.ListenAndServe(":8080", router)
1
2
3
4
5
6
7

一般使用router.Run,其底层就是封装了http原生的方式

# 二. gin快速入门

# 1. 响应

  • 响应字符串
func main() {
	router := gin.Default()
	router.GET("/", func(context *gin.Context) {
		//context.String(200, "hello") //相应的是字符串
		context.String(http.StatusOK, "hello") //code编码可以直接诶调用http里面的对象
	})

	router.Run(":8080")
}
1
2
3
4
5
6
7
8
9
  • 响应json

可以再响应字符串上面的拼接上json,并在response上加上application/json就可以,下面是另外一个方式

package main

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


type User struct {
	Username string
	Age      int
}

func main() {

	router := gin.Default()

	router.GET("/", func(context *gin.Context) {
		user := User{"zhengjian", 11}
		context.JSON(200, user)
	})

	router.Run(":8080")
}


//结果
{"Username":"zhengjian","Age":11}
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

如果我们想要控制json的key该怎么办呢?

这时候可以使用到结构体中的标签:

package main

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


type User struct {
  Username string `json:"username"`
  Age      int `json:"age"`
}

func main() {

	router := gin.Default()

	router.GET("/", func(context *gin.Context) {
		user := User{"zhengjian", 11}
		context.JSON(200, user)
	})

	router.Run(":8080")
}
//结果
{"username":"zhengjian","age":11}
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

那如何让某个字段不进行序列化呢?

type User struct {
  Username string `json:"username"`
  Age      int `json:"-"`
}

//结果
{"username":"zhengjian"}
1
2
3
4
5
6
7

看一下c.JSON的源码

func (c *Context) JSONP(code int, obj any) {
	callback := c.DefaultQuery("callback", "")
	if callback == "" {
		c.Render(code, render.JSON{Data: obj})
		return
	}
	c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
}
1
2
3
4
5
6
7
8

可以看到any这个对象最终也会使用render.JSON转化为json

我们看response的响应头就可以知道

content-Type: application/json; charset=utf-8

如果不是json,则显示text/plain; charset=utf-8

当然,这里传递不一定是传递一个结构体,也可以传递一个map对象

image-20230815160248004

  • 直接响应json
c.gin(200,gin.H{"username":"zj"})
1
  • 响应文件
c.file(fileName)
1
// 在golang总,没有相对文件的路径,它只有相对项目的路径
// 网页请求这个静态目录的前缀, 第二个参数是一个目录,注意,前缀不要重复
router.StaticFS("/static", http.Dir("static/static"))
// 配置单个文件, 网页请求的路由,文件的路径
router.StaticFile("/titian.png", "static/titian.png")
1
2
3
4
5

除了相应字符串和json,还可以显示xml和yaml

  • 响应xml
router.GET("/xml", func(c *gin.Context) {
  c.XML(http.StatusOK, gin.H{"user": "hanru", "message": "hey", "status": http.StatusOK})
})
1
2
3

  • 响应yaml
router.GET("/yaml", func(c *gin.Context) {
  c.YAML(http.StatusOK, gin.H{"user": "hanru", "message": "hey", "status": http.StatusOK})
})
1
2
3

image-20230815161345228

响应html

TODO 这个不重要,很少用,以后用到在看吧 TODO

# 如何进行重定向
router.GET("/redirect", func(c *gin.Context) {
    //支持内部和外部的重定向
    c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
})
1
2
3
4

# 2.如何获取前端数据

  • 怎么获得查询参数
package main

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

func main() {
	router := gin.Default()
	router.GET("/query", func(context *gin.Context) {
		fmt.Println(context.Query("username"))  //只会返回一个对应的value,如果username不存在,则value为空字符串
		fmt.Println(context.Get("age"))         //返回两个值 value bool   当参数体中不包含key时,bool为false
		fmt.Println(context.QueryArray("name")) //如果多个相同的key通过&连接起来,QueryArray可以拿到多个value
		fmt.Println(context.QueryMap("user"))   //拿到对应的key-value,类似requestBody
	})

	router.Run(":8080")
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 获取动态路径 类似rest风格
package main

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

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

	router.GET("/index/:username", func(context *gin.Context) {//在动态路径的前面加上:
		username := context.Param("username")
		fmt.Println(username)
	})
	router.Run(":8080")

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 怎么获取表单form参数

怎么获取表单的参数?表单一般是post

可以接收 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)
}
1
2
3
4
5
6
7
  • 直接获取原始数据

原始参数 GetRawData

为什么要学习这个原始参数,因为我们的传输大多数的数据都是以json格式来传输的,前端传递过来的数据也是json格式,所以我们也需要通过原始参数来获取对应的数据。

可以直接拿到body里面的字节数组,我们拿到字节数组过后可以通过string转化为对应的数据,但是不同的content-type对应的数据格式是不一样的。

form-data

----------------------------638149124879484626406689
Content-Disposition: form-data; name="name"

枫枫
----------------------------638149124879484626406689
Content-Disposition: form-data; name="name"

zhangsan
----------------------------638149124879484626406689
Content-Disposition: form-data; name="addr"

长沙市
----------------------------638149124879484626406689--
OK!
1
2
3
4
5
6
7
8
9
10
11
12
13
14

x-www-form-urlencoded

name=abc&age=23CopyErrorOK!
1

json

{
    "name": "枫枫",
    "age": 21
}

1
2
3
4
5
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)
  }
}

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

封装一个解析json到结构体上的函数

func bindJson(c *gin.Context, obj any) (err error) { //这里的any也可以用interface
  body, _ := c.GetRawData()
  contentType := c.GetHeader("Content-Type")
  switch contentType {
  case "application/json":
    err = json.Unmarshal(body, &obj) //将json转化为结构体
    if err != nil {
      fmt.Println(err.Error())
      return err
    }
  }
  return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13

怎么将json转化为对应的对象:

err = json.Unmarshal(body, &obj)

# 3. 如何设置请求头响应头

# 请求头相关

注意:对于response,可以直接调用header方法去设置相应头,但是对于request获取的话,最好可以使用context.request去得到对应的request的对应

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"))
  fmt.Println(c.Request.Header["User-Agent"])
  // 如果是用map的取值方式,请注意大小写问题
  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": "成功"})
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 响应头相关
// 设置响应头
router.GET("/res", func(c *gin.Context) {
  c.Header("Token", "jhgeu%hsg845jUIF83jh")
  c.Header("Content-Type", "application/text; charset=utf-8")
  c.JSON(0, gin.H{"data": "看看响应头"})
})
1
2
3
4
5
6

# 4.什么是绑定器

我们前面讲解了如何获取请求中的参数,但是如果我们的参数有很多很多,也通过刚刚这种方式的话就会非常繁琐,所以,这里通过绑定器,将前端的数据自动绑定到我们的结构体当中,只需要在结构体中的标签中加上对应的字段即可

#

注意,gin中如果想使用绑定器,必须对应的结构体中的字段设置为首字母大些可供外界访问才可以,不然会出现绑定后数据无法赋值到对应的对象中的情况。

# must Bind

基本不用,校验码失败会更改状态码

# should bind

可以绑定json、query、param、yaml 、xml

主要是前端传递进来的数据进行绑定

如果检验不通过会返回错误,如果我们不想要对应的错误,我们可以自定义对应的错误信息,后面会讲到。

# ①shouldBindJSON

在Gin这个Go语言的Web框架中,ShouldBindJSON是一个用于从HTTP请求中解析JSON数据并将其绑定到Go结构体的方法。它是Gin框架提供的一个功能,旨在简化从HTTP请求中提取数据并将其转换为Go结构体的过程。

具体来说,ShouldBindJSON方法会将HTTP请求体中的JSON数据解析并映射到一个指定的Go结构体。这使得开发者可以方便地将接收到的JSON数据与预定义的Go结构体进行匹配,从而更方便地进行数据验证和处理。

条件

  1. 首先,接收的数据必须是请求体里面的json
  2. 在结构体中使用tag将使用json标签定义对应前端JSON中的key
package main

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

type User struct {
    Username string `json:"username"`
    Email    string `json:"email"`
}

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

    router.POST("/user", func(c *gin.Context) {
        var user User

        // 使用 ShouldBindJSON 将请求中的 JSON 数据绑定到 User 结构体
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{"message": "User created", "user": user})
    })

    router.Run(":8080")
}

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
# ②ShouldBindQuery

主要用于绑定路由中拼接的参数

前提:首先接收的是请求参数的keyvalue

结构体的标签使用form标签。

// ?name=枫枫&age=21&sex=男
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("/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(":8080")
}

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
# ③ShouldBindUri

注意这里是在路径中取值,标签使用uri即可

// /uri/fengfeng/21/男

package main

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

type UserInfo struct {
  Name string `uri:"name"`
  Age  int    `uri:"age"`
  Sex  string `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")
}
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
# ④通用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(":8080")
}


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

注意上面的所有的绑定器,基本上都会自动的进行校验,例如结构体中定义的是int类型,但是接受的参数是string类型,那么自动校验会将错误提示返回给err

# 5.什么是验证器

如果我们使用了验证器,gin会自动帮我们进行数据类型的校验,如结构体中的int类型,而前端传递的是string,就会报错。

但是更多的时候我们会自定义更多的验证器,来验证前端传递的参数是否正确,这个时候,我们就可以在标签中对应的字段加上binding标签来标识。

// 不能为空,并且不能没有这个字段
required: 必填字段,如:binding:"required"  

// 针对字符串的长度
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:"-"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • 内置绑定器

type student struct{

​ username string bind:"oneof red green" //内置绑定器

}

// 枚举  只能是red 或green
oneof=red green  

// 字符串  
contains=fengfeng  // 包含fengfeng的字符串
excludes // 不包含
startswith  // 字符串前缀
endswith  // 字符串后缀

// 数组
dive  // dive后面的验证就是针对数组中的每一个元素

// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。
// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径

// 日期验证  1月2号下午3点4分5秒在2006年
datetime=2006-01-02
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 6.自定义验证器的错误信息

当验证不通过时,会给出错误的信息,但是原始的错误信息不太友好,不利于用户查看

只需要给结构体加一个msg 的tag

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:"邮箱地址格式不正确"`
}
CopyErrorOK!
1
2
3
4
5
6

当出现错误时,就可以来获取出错字段上的msg。

那么该怎么获取标签msg的值呢?这里会用到反射

  • err:这个参数为ShouldBindJSON返回的错误信息
  • obj:这个参数为绑定的结构体
  • 还有一点要注意的是,validator这个包要引用v10这个版本的,否则会出错
// 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()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 7. 自定义验证器

  1. 注册验证器函数
// github.com/go-playground/validator/v10
// 注意这个版本得是v10的

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
   v.RegisterValidation("sign", signValid)
}
1
2
3
4
5
6
  1. 编写函数
// 如果用户名不等于fengfeng就校验失败
func signValid(fl validator.FieldLevel) bool {
  name := fl.Field().Interface().(string)
  if name != "fengfeng" {
    return false
  }
  return true
}
1
2
3
4
5
6
7
8
  1. 使用
type UserInfo struct {
  Name string `json:"name" binding:"sign" msg:"用户名错误"`
  Age  int    `json:"age" binding:""`
}CopyErrorOK!
package main

import (
  "github.com/gin-gonic/gin"
  "github.com/gin-gonic/gin/binding"
  "github.com/go-playground/validator/v10"
  "reflect"
)

func GetValidMsg(err error, obj interface{}) string {
  // obj为结构体指针
  getObj := reflect.TypeOf(obj)
  // 断言为具体的类型,err是一个接口
  if errs, ok := err.(validator.ValidationErrors); ok {
    for _, e := range errs {
      if f, exist := getObj.Elem().FieldByName(e.Field()); exist {
        return f.Tag.Get("msg") //错误信息不需要全部返回,当找到第一个错误的信息时,就可以结束
      }
    }
  }
  return err.Error()
}
// 如果用户名不等于fengfeng就校验失败
func signValid(fl validator.FieldLevel) bool {
  name := fl.Field().Interface().(string)
  if name != "fengfeng" {
    return false
  }
  return true
}


func main() {
  router := gin.Default()
  router.POST("/", func(c *gin.Context) {
    type UserInfo struct {
      Name string `json:"name" binding:"sign" msg:"用户名错误"`
      Age  int    `json:"age" binding:""`
    }
    var user UserInfo
    err := c.ShouldBindJSON(&user)
    if err != nil {
      // 显示自定义的错误信息
      msg := GetValidMsg(err, &user)
      c.JSON(200, gin.H{"msg": msg})
      return
    }
    c.JSON(200, user)
  })
  // 注册
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    v.RegisterValidation("sign", signValid)
  }
  router.Run(":80")
}
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
59

# 8. 文件上传

image-20230816111214469

# 单文件
func main() {
  router := gin.Default()
  // 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
  // 单位是字节, << 是左移预算符号,等价于 8 * 2^20
  // gin对文件上传大小的默认值是32MB
  router.MaxMultipartMemory = 8 << 20  // 8 MiB
  router.POST("/upload", func(c *gin.Context) {
    // 单文件
    file, _ := c.FormFile("file")
    log.Println(file.Filename)

    dst := "./" + file.Filename
    // 上传文件至指定的完整文件路径
    c.SaveUploadedFile(file, dst)

    c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
  })
  router.Run(":8080")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 服务端保存文件的几种方式
# SaveUploadedFile
c.SaveUploadedFile(file, dst)  // 文件对象  文件路径,注意要从项目根路径开始写CopyErrorOK!
1
# Create+Copy

file.Open的第一个返回值就是我们讲文件对象中的那个文件(只读的),我们可以使用这个去直接读取文件内容

file, _ := c.FormFile("file")
log.Println(file.Filename)
// 读取文件中的数据,返回文件对象
fileRead, _ := file.Open()
dst := "./" + file.Filename
// 创建一个文件
out, err := os.Create(dst)
if err != nil {
  fmt.Println(err)
}
defer out.Close()
// 拷贝文件对象到out中
io.Copy(out, fileRead)CopyErrorOK!
1
2
3
4
5
6
7
8
9
10
11
12
13
# 读取上传的文件
file, _ := c.FormFile("file")
// 读取文件中的数据,返回文件对象
fileRead, _ := file.Open()
data, _ := io.ReadAll(fileRead)
fmt.Println(string(data))CopyErrorOK!
1
2
3
4
5

这里的玩法就很多了

例如我们可以基于文件中的内容,判断是否需要保存到服务器中

# 多文件上传
func main() {
  router := gin.Default()
  // 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
  router.MaxMultipartMemory = 8 << 20 // 8 MiB
  router.POST("/upload", func(c *gin.Context) {
    // Multipart form
    form, _ := c.MultipartForm()
    files := form.File["upload[]"]  // 注意这里名字不要对不上了

    for _, file := range files {
      log.Println(file.Filename)
      // 上传文件至指定目录
      c.SaveUploadedFile(file, "./"+file.Filename)
    }
    c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
  })
  router.Run(":8080")
}CopyErrorOK!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

image-20230816112339398

# 9.文件下载

# 直接响应一个路径下的文件
c.File("uploads/12.png")CopyErrorOK!
1

有些响应,比如图片,浏览器就会显示这个图片,而不是下载,所以我们需要使浏览器唤起下载行为

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")CopyErrorOK!
1
2
3
4

注意,文件下载浏览器可能会有缓存,这个要注意一下

解决办法就是加查询参数

# 10. 前后端模式下的文件下载

如果是前后端模式下,后端就只需要响应一个文件数据

文件名和其他信息就写在请求头中

c.Header("fileName", "xxx.png")
c.Header("msg", "文件下载成功")
c.File("uploads/12.png")CopyErrorOK!
1
2
3
# 前端写法
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对象
    })}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 11.gin中间件和路由

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 多个中间件

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
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
# 中间件拦截响应

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")
}
CopyErrorOK!
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
# 中间件放行

c.Next(),Next前后形成了其他语言中的请求中间件和响应中间件

其实可以看做责任链模式

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
*/CopyErrorOK!
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

image-20230816130912819

如果其中一个中间件响应了c.Abort(),后续中间件将不再执行,直接按照顺序走完所有的响应中间件

但是其实上面的中间件是一个路由对应一个中间件,没有达到复用的效果

下面是使用全局中间件,达到复用的效果

# 全局注册中间件
package main

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

func m10(c *gin.Context) {
  fmt.Println("m1 ...in")
  c.Next()
  fmt.Println("m1 ...out")
}

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

  router.Use(m10)
  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")

}
CopyErrorOK!
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

使用Use去注册全局中间件,Use接收的参数也是多个HandlerFunc

# 中间件传递数据

使用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")

}
CopyErrorOK!
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

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 := name.(User)
    fmt.Println(user.Name, user.Age)
    c.JSON(200, gin.H{"msg": "index"})
  })

  router.Run(":8080")

}
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
# 路由分组

将一系列的路由放到一个组下,统一管理

例如,以下的路由前面统一加上api的前缀

package main

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

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

  r := router.Group("/api")
  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")
}
CopyErrorOK!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 路由分组注册中间件
package main

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

func middle(c *gin.Context) {
  fmt.Println("middle ...in")
}

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

  r := router.Group("/api").Use(middle)  // 可以链式,也可以直接r.Use(middle)
  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")
}
CopyErrorOK!
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 (
  "fmt"
  "github.com/gin-gonic/gin"
)

func middle(c *gin.Context) {
  fmt.Println("middle ...in")
}
func middle1() gin.HandlerFunc {
  // 这里的代码是程序一开始就会执行
  return func(c *gin.Context) {
    // 这里是请求来了才会执行
    fmt.Println("middle1 ...inin")
  }
}

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

  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")
}
CopyErrorOK!
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
# gin.Default
func Default() *Engine {
  debugPrintWARNINGDefault()
  engine := New()
  engine.Use(Logger(), Recovery())
  return engine
}
CopyErrorOK!
1
2
3
4
5
6
7

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

Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。 Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。 如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

使用gin.New,如果不指定日志,那么在控制台中就不会有日志显示

# 中间件案例 (opens new window)

# 权限验证 (opens new window)

以前后端最流行的jwt为例,如果用户登录了,前端发来的每一次请求都会在请求头上携带上token

后台拿到这个token进行校验,验证是否过期,是否非法

如果通过就说明这个用户是登录过的

不通过就说明用户没有登录

package main

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

func JwtTokenMiddleware(c *gin.Context) {
  // 获取请求头的token
  token := c.GetHeader("token")
  // 调用jwt的验证函数
  if token == "1234" {
    // 验证通过
    c.Next()
    return
  }
  // 验证不通过
  c.JSON(200, gin.H{"msg": "权限验证失败"})
  c.Abort()
}

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

  api := router.Group("/api")

  apiUser := api.Group("")
  {
    apiUser.POST("login", func(c *gin.Context) {
      c.JSON(200, gin.H{"msg": "登录成功"})
    })
  }
  apiHome := api.Group("system").Use(JwtTokenMiddleware)
  {
    apiHome.GET("/index", func(c *gin.Context) {
      c.String(200, "index")
    })
    apiHome.GET("/home", func(c *gin.Context) {
      c.String(200, "home")
    })
  }

  router.Run(":8080")
}
CopyErrorOK!
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

# 耗时统计 (opens new window)

统计每一个视图函数的执行时间

func TimeMiddleware(c *gin.Context) {
  startTime := time.Now()
  c.Next()
  since := time.Since(startTime)
  // 获取当前请求所对应的函数
  f := c.HandlerName()
  fmt.Printf("函数 %s 耗时 %d\n", f, since)
}
1
2
3
4
5
6
7
8

# 日志

gin自带的有系统日志

如何将日志输出到文件

package main

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

func main() {
  // 输出到文件
  f, _ := os.Create("gin.log")  //创建一个文件
  //gin.DefaultWriter = io.MultiWriter(f)
  // 如果需要同时将日志写入文件和控制台,请使用以下代码。
  gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
  router := gin.Default()
  router.GET("/", func(c *gin.Context) {
    c.JSON(200, gin.H{"msg": "/"})
  })
  router.Run()
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • os.Create创建一个文件
  • gin.DefaultWriter = io.MultiWriter(f, os.Stdout) 设置gin的日志输出到文件和控制台

# 定义路由格式 (opens new window)

启动gin,它会显示所有的路由,默认格式如下

[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)CopyErrorOK!
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
*/CopyErrorOK!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 查看路由 (opens new window)

router.Routes()  // 它会返回已注册的路由列表CopyErrorOK!
1

# 环境切换 (opens new window)

img

如果不想看到这些debug日志,那么我们可以改为release模式

gin.SetMode(gin.ReleaseMode)
router := gin.Default()CopyErrorOK!
1
2

# 修改log的显示 (opens new window)

默认的是这样的

[GIN] 2022/12/11 - 14:22:00 | 200 |  0s |  127.0.0.1 | GET  "/"CopyErrorOK!
1

如果觉得不好看,我们可以自定义

package main

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

func LoggerWithFormatter(params gin.LogFormatterParams) string {

  return fmt.Sprintf(
    "[ feng ] %s  | %d | \t %s | %s | %s \t  %s\n",
    params.TimeStamp.Format("2006/01/02 - 15:04:05"),
    params.StatusCode,  // 状态码
    params.ClientIP,  // 客户端ip
    params.Latency,  // 请求耗时
    params.Method,  // 请求方法
    params.Path,  // 路径
  )
}
func main() {
  router := gin.New()
  router.Use(gin.LoggerWithFormatter(LoggerWithFormatter))
  router.Run()

}
CopyErrorOK!
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 LoggerWithFormatter(params gin.LogFormatterParams) string {
  return fmt.Sprintf(
    "[ feng ] %s  | %d | \t %s | %s | %s \t  %s\n",
    params.TimeStamp.Format("2006/01/02 - 15:04:05"),
    params.StatusCode,
    params.ClientIP,
    params.Latency,
    params.Method,
    params.Path,
  )
}
func main() {
  router := gin.New()
  router.Use(
    gin.LoggerWithConfig(
      gin.LoggerConfig{Formatter: LoggerWithFormatter},
    ),
  )
  router.Run()

}CopyErrorOK!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

但是你会发现自己这样输出之后,没有颜色了,不太好看,我们可以输出有颜色的log

func LoggerWithFormatter(params gin.LogFormatterParams) string {
  var statusColor, methodColor, resetColor string
  statusColor = params.StatusCodeColor()
  methodColor = params.MethodColor()
  resetColor = params.ResetColor()
  return fmt.Sprintf(
    "[ feng ] %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,
    params.ClientIP,
    params.Latency,
    methodColor, params.Method, resetColor,
    params.Path,
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 第三方日志框架

logrus

编辑 (opens new window)
上次更新: 2024/02/22, 14:03:19
垃圾回收与写屏障
go中flag的使用

← 垃圾回收与写屏障 go中flag的使用→

最近更新
01
spark基础
02-22
02
mysql读写分离和分库分表
02-22
03
数据库迁移
02-22
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式