Gin框架: 控制器, 中间件的分层设计案例

对控制器的分组与继承

1 )设计项目目录结构

tree 复制代码
yourGinProject/ ·······························  根目录
  ├── go.mod ··································  go mod 文件
  ├── go.sum ··································  go sum 文件
  ├── main.go ·································  main 文件
  └── tpls ····································· html模板目录
  │     └── web
  │     │    └── index.html
  ├── routers ·································· 路由目录
  │     ├── webRouters.go
  │     ├── apiRouters.go
  │     └── adminRouters.go
  ├── controllers ······························ 控制器目录
  │     ├── web
  │     │     └── webCtrl.go
  │     ├── api
  │     │     └── apiCtrl.go
  │     └── admin
  │     │     ├── base.go
  │     │     ├── indexCtrl.go
  │     │     └── userCtrl.go

2 )主程序 main.go

go 复制代码
package main

import (
	"gin-demo/routers" //gin-demo 是 go mod init 初始化的工程,下同
	"github.com/gin-gonic/gin"
)

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()

	//加载模板 放在配置路由前面
	r.LoadHTMLGlob("tpls/**/*")

	routers.WebRoutersInit(r)
	routers.ApiRoutersInit(r)
	routers.AdminRoutersInit(r)

	r.Run()
}

3 ) HTML模板目录配置

tpls/web/index.html

html 复制代码
{{ define "web/index.html" }}

  <h1>web index 页面</h1>

  {{.msg}}

{{ end }}

4 ) routers 配置

4.1 webRouters.go

go 复制代码
package routers

import (
	"gin-demo/controllers/web"

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

func WebRoutersInit(r *gin.Engine) {
	webRouters := r.Group("/")
	{
		webRouters.GET("/", web.WebCtrl{}.Index)
	}
}

4.2 apiRouters.go

go 复制代码
package routers

import (
	"gin-demo/controllers/api"
	"github.com/gin-gonic/gin"
)

func ApiRoutersInit(r *gin.Engine) {
	apiRouters := r.Group("/api")
	{
		apiRouters.GET("/", api.ApiCtrl{}.Index)
		apiRouters.GET("/user", api.ApiCtrl{}.User)
	}
}

4.2 adminRouters.go

go 复制代码
package routers

import (
	"gin-demo/controllers/admin"
	"github.com/gin-gonic/gin"
)

func AdminRoutersInit(r *gin.Engine) {
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", admin.IndexCtrl{}.Index)
		adminRouters.GET("/user", admin.UserCtrl{}.Index)
		adminRouters.GET("/user/success", admin.UserCtrl{}.Success)
		adminRouters.GET("/user/error", admin.UserCtrl{}.Error)
	}
}

5 ) controller 配置

5.1 web/webCtrl.go

go 复制代码
package web

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

type WebCtrl struct{}

func (con WebCtrl) Index(c *gin.Context) {
	c.HTML(http.StatusOK, "web/index.html", gin.H{
		"msg": "我是一个msg",
	})
}

5.2 api/apiCtrl.go

go 复制代码
package api

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

type ApiCtrl struct{}

func (con ApiCtrl) Index(c *gin.Context) {
	c.String(http.StatusOK, "api接口总承")
}
func (con ApiCtrl) User(c *gin.Context) {
	c.String(http.StatusOK, "这是一个 user 接口")
}

5.3 admin/indexCtrl.go

go 复制代码
package admin

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

type IndexCtrl struct {}

func (con IndexCtrl) Index(c *gin.Context) {
	c.String(http.StatusOK, "admin 页面")
}

5.4 admin/baseCtrl.go

go 复制代码
package admin

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

type BaseCtrl struct{}

func (con BaseCtrl) success(c *gin.Context) {
	c.String(http.StatusOK, "成功")
}

func (con BaseCtrl) error(c *gin.Context) {
	c.String(http.StatusOK, "失败")
}

5.4 admin/userCtrl.go

go 复制代码
package admin

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

type UserCtrl struct {
	BaseCtrl
}

func (con UserCtrl) Index(c *gin.Context) {
	c.String(http.StatusOK, "user 页面")
}
func (con UserCtrl) Success(c *gin.Context) {
	con.success(c)
}
func (con UserCtrl) Error(c *gin.Context) {
	con.error(c)
}

以上就是对控制器的一般文件拆分和继承关系的调用示例,验证如下

/ 访问首页
/api
/api/user
/admin
/admin/user
/admin/user/success
/admin/user/error

以上均可正常访问,这样就可以最快完成一个项目的拆分

中间件的处理

1 ) 基础用法, 单一中间件

go 复制代码
package main

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

func initMiddleware(c *gin.Context) {
	// 记录开始时间
	start := time.Now().UnixNano()
	// 调用该请求的剩余处理程序
	c.Next()
	// 记录结束时间
	end := time.Now().UnixNano()
	// 输出当前渲染时间差
	fmt.Println("时间:", end - start)
}

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()

	r.GET("/", initMiddleware, func(c *gin.Context) {
		c.String(http.StatusOK, "首页")
	})

	r.GET("/news", initMiddleware, func(c *gin.Context) {
		c.String(http.StatusOK, "新闻页面")
	})

	r.Run()
}
  • 中间件就是匹配路由前和匹配路由完成后执行的一系列操作
  • 中间件必须是一个 gin.HandlerFunc 类型

2 )多个路由中间件

go 复制代码
package main

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

func initMiddleware(c *gin.Context) {
	fmt.Println("第1个中间件开始")
	// 记录开始时间
	start := time.Now().UnixNano()
	// 调用该请求的剩余处理程序
	c.Next()
	// 记录结束时间
	end := time.Now().UnixNano()
	// 输出当前渲染时间差
	fmt.Println("第1个中间件结束,并统计其处理时间:", end - start)
}

func initMiddleware2(c *gin.Context) {
	fmt.Println("第2个中间件开始")
	c.Next()
	// 终止调用该请求的剩余处理程序
	// c.Abort() // 注意,Next 和 Abort 只能二选一,可以控制在某些情况下,终止中间件
	fmt.Println("第2个中间件结束")
}

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()

	r.GET("/", initMiddleware, initMiddleware2, func(c *gin.Context) {
		c.String(http.StatusOK, "首页")
	})

	r.GET("/news", initMiddleware, initMiddleware2, func(c *gin.Context) {
		c.String(http.StatusOK, "新闻页面")
	})

	r.Run()
}
  • 上述示例中,有两个中间件,就是 initMiddleware, initMiddleware2

  • 访问路由时的输出顺序

    第1个中间件开始
    第2个中间件开始
    第2个中间件结束
    第1个中间件结束,并统计其处理时间: 21000
    
  • 这种就是洋葱模型,基本上所有中间件都符合这一模型

  • 配置路由的时候可以传递多个 func 回调函数

  • 最后一个 func 回调函数前面触发的方法都可以称为中间件

  • 中间件里面加上 ctx.Next()可以让我们在路由匹配完成后执行一些操作

  • 如果想要终止中间件操作可以通过判断,添加 ctx.Abort() 来终止接下来的操作

3 )全局中间件

go 复制代码
package main

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

func initMiddleware(c *gin.Context) {
	fmt.Println("第1个中间件开始")
	// 记录开始时间
	start := time.Now().UnixNano()
	// 调用该请求的剩余处理程序
	c.Next()
	// 记录结束时间
	end := time.Now().UnixNano()
	// 输出当前渲染时间差
	fmt.Println("第1个中间件结束,并统计其处理时间:", end - start)
}

func initMiddleware2(c *gin.Context) {
	fmt.Println("第2个中间件开始")
	c.Next()
	fmt.Println("第2个中间件结束")
}

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()

	// 全局中间件
	r.Use(initMiddleware, initMiddleware2)

	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "首页")
	})

	r.GET("/news", func(c *gin.Context) {
		c.String(http.StatusOK, "新闻页面")
	})

	r.Run()
}
  • 这种属于全局配置的中间件,不用在每个路由中书写,进行全局use
  • 这种写法和第2种效果一致

4 )中间件的拆分

tree 复制代码
yourGinProject/ ·······························  根目录
  ├── go.mod ··································  go mod 文件
  ├── go.sum ··································  go sum 文件
  ├── main.go ·································  main 文件
  └── tpls ····································· html模板目录
  │     └── web
  │     │    └── index.html
  ├── routers ·································· 路由目录
  │     ├── webRouters.go
  │     ├── apiRouters.go
  │     └── adminRouters.go
  ├── controllers ······························ 控制器目录
  │     ├── web
  │     │     └── webCtrl.go
  │     ├── api
  │     │     └── apiCtrl.go
  │     └── admin
  │     │     ├── base.go
  │     │     ├── indexCtrl.go
  │     │     └── userCtrl.go
  ├── middlewares ······························ 中间件目录
  │     └── init.go

这里使用最顶层控制器拆分时用的结构

这里 middlewares/init.go

go 复制代码
package middlewares

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

func InitMiddleware(c *gin.Context) {
	//判断用户是否登录

	fmt.Println("当前时间:", time.Now())
	fmt.Println("当前URL:", c.Request.URL)

	c.Set("username", "Wang") // 在请求上下文中设置值,后续的处理函数能够取到该值

	// 定义一个 goroutine 统计日志
	// 当在中间件或 handler 中启动新的 goroutine 时
	// 不能使用原始的上下文(c *gin.Context), 必须使用其只读副本(c.Copy())
	cCp := c.Copy()
	go func() {
		time.Sleep(2 * time.Second)
		fmt.Println("Done! in path " + cCp.Request.URL.Path)
	}()
}

改造 routers/adminRouters.go 文件

go 复制代码
package routers

import (
	"gin-demo/controllers/admin"
	"github.com/gin-gonic/gin"
	"gin-demo/middlewares" // 引入
)

func AdminRoutersInit(r *gin.Engine) {
	adminRouters := r.Group("/admin", middlewares.InitMiddleware) // 注意这里
	{
		adminRouters.GET("/", admin.IndexCtrl{}.Index)
		adminRouters.GET("/user", admin.UserCtrl{}.Index)
		adminRouters.GET("/user/success", admin.UserCtrl{}.Success)
		adminRouters.GET("/user/error", admin.UserCtrl{}.Error)
	}
}

/admin 及子路由 被访问时都会经过这个中间件

这里用了一个 goroutine 做数据统计,下面在 admin.userCtrl 中获取中间件中配置的值

改造 controllers/admin/userCtrl.go 文件

go 复制代码
package admin

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

type UserCtrl struct {
	BaseCtrl
}

func (con UserCtrl) Index(c *gin.Context) {
	username, _ := c.Get("username") // 这里从中间件中读取数据
	c.String(http.StatusOK, "user 页面: %v", username) // 响应出去
}
func (con UserCtrl) Success(c *gin.Context) {
	con.success(c)
}
func (con UserCtrl) Error(c *gin.Context) {
	con.error(c)
}

这样就可以获取到中间件中读取的数据了

注意事项

  • gin 默认中间件
    • gin.Default()默认使用了 Logger 和 Recovery 中间件,其中:
    • Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release
    • Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500 响应码
    • 如果不想使用上面两个默认的中间件,可以使用 gin.New() 新建一个没有任何默认中间件的路由
  • gin 中间件中使用 goroutine
    • 当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context)
    • 必须使用其只读副本(c.Copy())
相关推荐
LightOfNight3 小时前
【设计模式】创建型模式之单例模式(饿汉式 懒汉式 Golang实现)
单例模式·设计模式·golang
MelonTe8 小时前
Golang网络模型netpoll源码解析
golang
Clown9511 小时前
go-zero(十) 数据缓存和Redis使用
redis·缓存·golang
蚂蚁在飞-12 小时前
一个高度可扩展的 Golang ORM 库【GORM】
数据库·oracle·golang
hummhumm13 小时前
第33章 - Go语言 云原生开发
java·开发语言·后端·python·sql·云原生·golang
凡人的AI工具箱14 小时前
40分钟学 Go 语言高并发:【实战】并发安全的配置管理器(功能扩展)
开发语言·后端·安全·架构·golang
桃园码工16 小时前
3-测试go-redis+redsync实现分布式锁 --开源项目obtain_data测试
redis·分布式·golang
熬了夜的程序员19 小时前
使用Go语言实现线程安全的Map
安全·golang
蚂蚁在飞-20 小时前
云原生时代的轻量级反向代理Traefik
云原生·golang
techdashen21 小时前
Go与黑客(第一部分)
开发语言·后端·golang