目录常用结构图

├── main.go:应用程序入口文件
├── controllers:控制器目录,用于处理请求
│ ├── home_controller.go:处理首页请求的控制器
│ ├── user_controller.go:处理用户请求的控制器
│ └── …
├── models:模型目录,用于封装数据操作
│ ├── user.go:用户模型,用于操作用户数据
│ └── …
├── routes:路由目录,用于配置路由
│ ├── home_routes.go:配置首页路由
│ ├── user_routes.go:配置用户路由
│ └── …
├── services:服务目录,用于封装业务逻辑
│ ├── user_service.go:用户服务,用于处理用户相关业务逻辑
│ └── …
├── middlewares:中间件目录,用于处理请求前后的逻辑
│ ├── auth.go:身份认证中间件,用于检查用户是否登录
│ ├── logger.go:日志中间件,用于记录请求日志
│ └── …
├── static:静态文件目录,用于存放静态文件,如图片、CSS、JS等
│ ├── css:CSS文件目录
│ ├── js:JS文件目录
│ ├── img:图片文件目录
│ └── …
├── templates:模板目录,用于存放HTML模板文件
│ ├── home.html:首页模板文件
│ ├── user.html:用户模板文件
│ └── …
├── conf:配置文件目录,用于存放应用程序的配置文件
│ ├── app.conf:应用程序的基本配置文件,如端口号、数据库连接等
│ └── …
└── vendor:依赖包目录,用于存放应用程序的依赖包

快速入门

参考: https://gin-gonic.com/zh-cn/docs/quickstart/

要求

Go 1.13 及以上版本

安装

要安装 Gin 软件包,需要先安装 Go 并设置 Go 工作区。

1.下载并安装 gin:

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

2.将 gin 引入到代码中:

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

3.(可选)如果使用诸如 http.StatusOK 之类的常量,则需要引入 net/http 包:

import "net/http"

开始

首先,创建一个名为 example.go 的文件

touch example.go

接下来, 将如下的代码写入 example.go 中:

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

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

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

然后, 执行 go run example.go 命令来运行代码:

1
2
# 运行 example.go 并且在浏览器中访问 HOST_IP:8080/ping
$ go run example.go

注意:

  • 使用gin.Default()创建路由时, 已经默认引入了Logger(), Recovery() 这两个中间件, 不需要重复设置。 这两个中间件的功能分别是日志和错误抛出。
    • Logger 中间件将日志写入gin.DefaultWriter, 即使配置了 GIN_MODE=release。
    • Recovery中间件会recover任何panic。如果有panic的话, 会写入500响应码。 也就是说,它可以防止程序因panic问题崩溃, 并且会在发生panic问题时返回500的响应码

      在 gin 之外, 我们也常在goroutine中, 使用一个recover()函数来捕获异常。

      此场景下, recover函数的作用:

      • 使用匿名函数捕获test抛出的panic
      • 是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。
      • recover仅在延迟函数中有效。
      • 在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果。
      • 如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。recover只有在defer调⽤的函数中有效。
  • 如果不想要默认中间件, 可以使用gin.New()来创建路由。

使用

可以返回的请求

  • ctx.JSON()

    可以是结构体map键值对, gin会自动将其转换为json数据进行传输。(记得,如果使用结构体,记得利用tag标签注释来设置变量名的小写- key为json,value为变量名小写, 多个tag用空格隔开)

  • ctx.JSONP()

    可以是结构体map键值对, gin会自动将其转换为json数据进行传输。(记得,如果使用结构体,记得利用tag标签注释来设置变量名的小写- key为json,value为变量名小写, 多个tag用空格隔开)

    • jsonp的api在前端请求时, 是可以有回调方法的。比如,可以利用jsonp来解决跨域问题
  • ctx.XML()

    可以是结构体map键值对, gin会自动将其转换为json数据进行传输。(记得,如果使用结构体,记得利用tag标签注释来设置变量名的小写- key为xml,value为变量名小写, 多个tag用空格隔开)

  • ctx.String()

获取前端的传值

  • 获取GET的传值

    • ctx.Query()
    • ctx.DefaultQuery() -> 带默认值, 可以设置前端为传递该值时,对应变量的默认值
  • 获取post的传值

    • ctx.PostForm()
    • ctx.DefaultPostForm() -> 带默认值, 可以设置前端为传递该值时,对应变量的默认值
    • ctx.ShouldBind()

      注意:

      第一点:
      在Gin框架中,PostForm()方法用于获取表单数据。然而,如果你使用axios发送请求,并且请求的Content-Type被设置为application/json,那么你可能无法使用PostForm()方法获取数据,因为PostForm()方法只能获取Content-Typeapplication/x-www-form-urlencodedmultipart/form-data的数据。

      如果你的请求的Content-Typeapplication/json,你应该使用Bind()或者ShouldBind()方法来获取请求体中的JSON数据。例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      type MyData struct {
      Field1 string `json:"field1"`
      Field2 string `json:"field2"`
      }

      func MyHandler(c *gin.Context) {
      var data MyData
      if err := c.ShouldBindJSON(&data); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
      }
      // 现在你可以使用data.Field1和data.Field2来访问你的数据
      }

      在这个例子中,如果你发送一个包含field1field2字段的JSON对象到这个处理器,那么这些字段的值将被绑定到data变量中。

      前端axios对于Post请求, 只能使用json, 因此用PostForm无法正常获取, 只能通过这种绑定结构体的方式获取。

      注意:

      第二点:
      在 Gin 框架中,ShouldBind 方法在第一次调用后,如果再次尝试读取 request body 的数据,会出现 EOF 的错误,因为 c.Request.Body 不可以重用。这是因为在 ShouldBind 方法调用过一次之后,context.request.body.sawEOF 的值是 false

      如果你需要多次绑定多个变量,你需要使用 ShouldBindBodyWith 方法。ShouldBindBodyWith 方法可以支持重复绑定,原理就是将 body 的数据缓存了下来。但是在二次取数据的时候还是得用 ShouldBindBodyWith 才行,直接用 ShouldBind 还是会报错的。

      ShouldBindBodyWith 方法被用来多次绑定 request body 到不同的结构体中。请注意,你需要根据你的需求来选择合适的绑定方法。如果你只需要绑定一次,ShouldBindShouldBindJSON 等方法可能更简单。如果你需要多次绑定,那么 ShouldBindBodyWith 将是一个更好的选择。

      总结

      • https://pkg.go.dev/github.com/gin-gonic/gin@v1.9.1#Context.ShouldBind
      • 如果你需要更好的性能, 则ShouldBindWith系列更有性价比, 但它只能使用一次。 (包括ShouldBindShouldBindJSON等省略第二个参数的简化使用版)
      • 如果你需要多次绑定使用, 那么ShouldBindBodyWithShouldBindWith 类似,但它将请求正文存储到上下文中,并在再次调用时重用。(仅此一个,无简化使用版)
  • 获取的 GET POST 值可以直接绑定到结构体上

    1. 利用tag标签注释来设置结构体内变量名的小写- key为form,value为变量名小写, 多个tag用空格隔开)
    2. 使用ctx.ShouldBind()
  • 获取 POST XML 数据

    1. 在结构体标签(tag)的key的其中一个, 使用xml来指定, value用变量名的小写。
    2. 使用ctx.GetRawData(), 得到的数据是xml的字节切片。
    3. 使用xml.Unmarshal(), 将字节切片转换至对应结构体。
  • 动态路由参数传值

    比较美观, 可以在没有 ? 的条件下传值

    1
    2
    3
    4
    r.GET("/xxx/:uid", func(ctx *gin.Context){
    uid := ctx.Param("uid")
    c.String(200, "uid=%s", uid)
    })

    下方这个代码片段, 参考自https://gin-gonic.com/zh-cn/docs/examples/param-in-path/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func main() {
    router := gin.Default()

    // 此 handler 将匹配 /user/john 但不会匹配 /user/ 或者 /user
    router.GET("/user/:name", func(c *gin.Context) {
    name := c.Param("name")
    c.String(http.StatusOK, "Hello %s", name)
    })

    // 此 handler 将匹配 /user/john/ 和 /user/john/send
    // 如果没有其他路由匹配 /user/john,它将重定向到 /user/john/
    router.GET("/user/:name/*action", func(c *gin.Context) {
    name := c.Param("name")
    action := c.Param("action")
    message := name + " is " + action
    c.String(http.StatusOK, message)
    })

    router.Run(":8080")
    }

路由分组

参考自https://gin-gonic.com/zh-cn/docs/examples/grouping-routes/

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
func main() {
router := gin.Default()

// 简单的路由组: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// 路由组值v1 -> 相当于分组前:
// router.POST("/v1/login", loginEndpoint)
// router.POST("/v1/submit", submitEndpoint)
// router.POST("/v1/read", readEndpoint)

// 简单的路由组: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}

router.Run(":8080")
}

当然, 我们也可以将某个路由分组, 从主文件中剥离至新的 package 所代表的一系列文件中。

1
2
3
4
5
6
7
8
9
10
11
12
// 某个router分组的文件
package routers

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

func Aaa(router *gin.Engine){
v1 := router.Group("/v1"){
v1.POST("/login", loginEndpoint)
// ...
// ...
}
}

然后在主文件中引入即可

1
2
3
4
5
6
7
8
9
// main文件

// 引入
import "mod包名/routers"

// 使用
routers.Aaa(router)
// ...
// ...

对路由的业务方法进行剥离(控制器抽离)

https://www.bilibili.com/video/BV1fA411F7aM?p=7&spm_id_from=pageDriver

跳转 3min 时刻, 查看

中间件

通俗来将, 这里的中间件指的就是, 匹配路由之前, 以及匹配路由之后,执行的一系列操作。

路由中间件

每个路由api的最后一个方法作为业务方法, 之前的统统作为路由中间间方法, 在业务方法之前执行。

在路由中间件方法中,可以使用 ctx.Next() 方法, 来使路由中间件中此方法内 ctx.Next()后的逻辑, 放到最后再执行。

ctx.Abort(), 可以使此中间件外, 之后的其他中间件不再执行; 但此中间件方法内 ctx.Abort之后的逻辑依然会继续执行完毕。

ctx.Next(), 在多个中间件顺序使用时, 可以起到嵌套的作用。

全局中间件

路由使用前, 可在router.Use()的形参内, 配置全局中间件。

全局中间件可作用于所有路由。

路由分组中间件

两种方法

  • 直接在 router.Group() 的形参中配置路由分组中间件(推荐使用这种方式)
  • router.Group()之后, 并且组内路由配置之前,使用 routerGroup.Use()来配置路由分组中间件

对中间件分组中的中间件方法进行剥离()

https://www.bilibili.com/video/BV1fA411F7aM?p=8

跳转 22min 时刻, 查看

中间件方法路由业务方法(控制器handler)之间如何共享数据

  • 使用 ctx.Set() 来设置值
  • 使用 ctx.Get() 来获取值
    • 由于此函数返回值是一个空接口类型, 所以使用前需要进行类型断言 变量.(具体类型)

中间件方法路由业务方法(控制器handler)使用goroutine

若在中间件方法路由业务方法(控制器handler)使用goroutine的话, 不能使用原始上下文(ctx *gin.Context), 必须使用其只读副本(c.Copy())

gin框架的使用中, 常会在输出日志等一些场景中, 会用到goroutine来并发进行日志的输出记录。

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 main() {
r := gin.Default()

r.GET("/long_async", func(c *gin.Context) {
// 创建在 goroutine 中使用的副本
cCp := c.Copy()
go func() {
// 用 time.Sleep() 模拟一个长任务。
time.Sleep(5 * time.Second)

// 请注意您使用的是复制的上下文 "cCp",这一点很重要
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})

r.GET("/long_sync", func(c *gin.Context) {
// 用 time.Sleep() 模拟一个长任务。
time.Sleep(5 * time.Second)

// 因为没有使用 goroutine,不需要拷贝上下文
log.Println("Done! in path " + c.Request.URL.Path)
})

// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}

自定义模板函数(自定义Model) - 用于html中的方法

自定义模板函数(自定义Model) - 用于html中的方法, 需要使用router.SetFuncMap(template.FuncMap{ }) 来注册。
这种业务逻辑我们同样也可以放到控制器函数内执行。
如果我们的应用非常简单的话, 我们可以在Controller 里面处理常见的业务逻辑。但是如果我们有一个功能想在多个控制器、或者多个模板里面复用的话, 那么我们就可以把公共的功能单独抽取出来作为一个模块(Model)。Model是逐步抽象的过程, 一般我们会在Model里面封装一些公共的方法让不同Controller以及html模板使用, 也可以在Model中实现和数据库打交道。

对于图中formatAsDate()函数这样的共用业务逻辑, 我们也可以统一将其抽离到models文件夹下, 用文件夹名字命名 package , 以便统一后端的mvc架构。

M V C 中:

  • M(Model)模型层: 即代表我们本小节的model, 主要用于处理数据库层, 以及封装一些V和C共用的 方法 (后端主要用于处理数据库模型给Controller提供crud的API操作)(在后端对于公共方法, 一般会单独再弄一个utils文件来处理或者单独弄一个common来处理, models仅处理数据库相关)。
  • V(View)视图层: 代表html模板(前后端分离时,后端可以忽略), 主要用于处理用户界面, 放置渲染用户界面用的逻辑文件。
  • C(Controller)控制层: 就是控制器 用于放置路由分组中用到的 handler业务逻辑。(常使用controllers或者是services做其文件夹名)
    • 用于处理用户对View的操作, 响应View的事件调用。
    • 同时还需要调用Model的接口, 从而修改数据。

以前在web的开发中html一般由后端渲染, 现今由于前后端分离 加上 前端的飞速发展, html的渲染早以转由前端工程师来负责, 并借鉴 .net wpf中 使用的mvvm框架, 发展出了 vue、react等一系列前端框架, 随着这些前端框架的不断壮大, mvc架构也就在前端逐渐没落了。(前端渲染的唯一弊端是SEO困难, 因此如果是项目官网的话, 一般还是交由后端来渲染)

题外话, 前端

目前, 前端开发已经普及了mvvm架构, 通过vm 与 v 直接的双向绑定封装, 使得业务开发更为简洁清晰, 将本来 C 层频繁操作dom等渲染 V 层的逻辑,通过mvvm架构层封装的数据绑定来隐形驱动, 简化了业务逻辑。