Gin、GORM 中责任链模式的使用
责任链模式在 GoF 的《设计模式》一书中是这样定义的
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
解耦请求的发送和接收,让多个接收对象都有机会处理这个请求。请求在多个串联的对象中传递,直到有对象能处理它。看下面这个例子
package main
import "fmt"
type HandlerFunc func(*Request) bool
type Request struct {
url string
handlers []HandlerFunc
}
func (r *Request) Use(handlerFunc HandlerFunc) {
r.handlers = append(r.handlers, handlerFunc)
}
func (r *Request) Run() {
for _, handler := range r.handlers {
if handler(r) {
break
}
}
}
// 测试
// 输出 2
func main() {
r := &Request{}
r.Use(func(r *Request) bool {
return false
})
r.Use(func(r *Request) bool {
fmt.Print(2, " ")
return true
})
r.Use(func(r *Request) bool {
fmt.Print(3, " ")
return true
})
r.Run()
}
例子中定义了一个请求 Request,请求可以被多个 HandlerFunc 处理,如果某个 HandlerFunc 处理成功则结束处理,其中 HandlerFunc 可以通过 Request.Use() 方法进行添加。
不过责任链模式在实际使用时,更多的是另一种变体,请求会被串联的多个对象依次处理,而不是被某个对象处理后就停止处理了。类似这样
package main
import "fmt"
type HandlerFunc func(*Request)
type Request struct {
url string
handlers []HandlerFunc
}
func (r *Request) Use(handlerFunc HandlerFunc) {
r.handlers = append(r.handlers, handlerFunc)
}
func (r *Request) Run() {
for _, handler := range r.handlers {
handler(r)
}
}
// 测试
// 输出 1 2 3
func main() {
r := &Request{}
r.Use(func(r *Request) {
fmt.Print(1, " ")
})
r.Use(func(r *Request) {
fmt.Print(2, " ")
})
r.Use(func(r *Request) {
fmt.Print(3, " ")
})
r.Run()
}
注意:本文内容基于以下环境编写
- Go 1.15.15
- Gin 1.7.2
- GORM 1.21.12
Gin 中责任链模式的使用
Gin 框架中间件的实现用到了责任链模式,责任链什么时候开始处理的呢?
在解决这个问题之前,我们先简单了解下 Gin 在整个 HTTP 请求处理过程中所扮演的角色。在不使用三方框架或者类库时,我们一般会直接使用 net/http 编写 HTTP 应用
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
if err := http.ListenAndServe(":8000", nil); err != nil {
fmt.Println("start http server fail:", err)
}
}
核心在于
func ListenAndServe(addr string, handler Handler) error
该方法会执行标准的 socket 连接过程,bind -> listen -> accept,当 accept 时启动新的协程处理客户端 socket,调用 Handler.ServeHTTP() 方法,处理结束后返回响应。
上面例子中传入的 Handler 为 nil,会默认使用 DefaultServeMux 作为 Handler,而 http.HandleFunc() 方法则是给 DefaultServeMux 添加一条路由以及对应的处理函数,当 DefaultServeMux.ServeHTTP() 被调用时,则会查找路由并执行对应的处理函数。因此,可以将 DefaultServeMux 的作用简单理解为路由注册、路由匹配、请求处理。
如果使用 Gin 呢,可以这样编写应用
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello World")
})
r.Run() // listen and serve on 0.0.0.0:8080
}
查看 r.Run() 方法
func (engine *Engine) Run(addr ...string) (err error) {
// ...
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
可以发现 http.ListenAndServe() 方法使用了 engine 作为 Handler,即 engine 取代 DefaultServeMux 了来处理路由注册、路由匹配、请求处理等任务。现在直接看 Engine.ServeHTTP()
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ...
engine.handleHTTPRequest(c)
// ...
}
func (engine *Engine) handleHTTPRequest(c *Context) {
// ...
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
// ...
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
// ...
break
}
// ...
}
上面方法可以暂时理解为路由匹配后得到了对应的 HandlerFunc 列表(表示注册的中间件或者路由处理方法,从这里可以看出所谓的中间件和路由处理方法其实是相同类型的),然后调用 c.Next() 开始处理责任链
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
咋一看还不大好理解,我们改造下开始的例子
package main
import "fmt"
type HandlerFunc func(*Request)
type Request struct {
url string
handlers []HandlerFunc
index int // 新增
}
func (r *Request) Use(handlerFunc HandlerFunc) {
r.handlers = append(r.handlers, handlerFunc)
}
// 新增
func (r *Request) Next() {
r.index++
for r.index < len(r.handlers) {
r.handlers[r.index](r)
r.index++
}
}
// 修改
func (r *Request) Run() {
r.index = -1
r.Next()
}
// 测试
// 输出 1 2 3 11
func main() {
r := &Request{}
r.Use(func(r *Request) {
fmt.Print(1, " ")
r.Next()
fmt.Print(11, " ")
})
r.Use(func(r *Request) {
fmt.Print(2, " ")
})
r.Use(func(r *Request) {
fmt.Print(3, " ")
})
r.Run()
}
注意这几个点
- 首先在 Request 结构体中新增了 index 属性,用于记录当前执行到了第几个 HandlerFunc
- 然后新增 Next() 方法支持手动调用责任链中之后的 HandlerFunc
- 最后修改 Run() 方法,通过调用 Next() 触发责任链处理
这个例子理解了后,Gin 的责任链触发也就基本清楚了。
另外需要注意的是,Gin 框架中 handlers 和 index 信息放在了 Context 里面
type Context struct {
// ...
handlers HandlersChain
index int8
engine *Engine
// ...
}
其中,HandlersChain 就是一个 HandlerFunc 切片
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
GORM 中责任链模式的使用
GORM 中增删改查都会涉及到责任链模式的使用,比如 Create()、Delete()、Update()、First() 等等,这里以 First() 为例跟下
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) {
// ...
return tx.callbacks.Query().Execute(tx)
}
tx.callbacks.Query() 返回 processor 对象,然后执行其 Execute() 方法
func (cs *callbacks) Query() *processor {
return cs.processors["query"]
}
func (p *processor) Execute(db *DB) *DB {
// ...
for _, f := range p.fns {
f(db)
}
// ...
}
就是在这个位置调用了与操作类型绑定的处理函数。嗯?操作类型是啥意思?对应的处理函数又有哪些?想解决这几个问题,需要搞清楚 callbacks 的定义、初始化、注册。
callbacks 定义如下
type callbacks struct {
processors map[string]*processor
}
type processor struct {
db *DB
// ...
fns []func(*DB)
callbacks []*callback
}
type callback struct {
name string
// ...
handler func(*DB)
processor *processor
}
在注册完毕后,值类似这样
// ---- callbacks 结构体属性 ----
// processors
{
"create": processorCreate,
"query": ...,
"update": ...,
"delete": ...,
"row": ...,
"raw": ...,
}
// ---- processor 结构体属性(processorCreate) ----
// callbacks 中有 3 个 callback
{name: gorm:query, handler: Query}
{name: gorm:preload, handler: Preload}
{name: gorm:after_query, handler: AfterQuery}
// fns 对应 3 个 callback 中的 handler,不过是排过序后的
callbacks 在 callbacks.go/initializeCallbacks() 中进行初始化
func initializeCallbacks(db *DB) *callbacks {
return &callbacks{
processors: map[string]*processor{
"create": {db: db},
"query": {db: db},
"update": {db: db},
"delete": {db: db},
"row": {db: db},
"raw": {db: db},
},
}
}
在 callbacks/callbacks.go/RegisterDefaultCallbacks 中进行注册(为了简洁,所贴代码只保留了 query 类型的回调注册)
func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
// ...
queryCallback := db.Callback().Query()
queryCallback.Register("gorm:query", Query)
queryCallback.Register("gorm:preload", Preload)
queryCallback.Register("gorm:after_query", AfterQuery)
if len(config.QueryClauses) == 0 {
config.QueryClauses = queryClauses
}
queryCallback.Clauses = config.QueryClauses
// ...
}
到这里,前面提出的 2 个问题也就迎刃而解了。所谓操作类型是指增删改查等操作,比如 create、delete、update、query 等等;每种操作类型绑定多个处理函数,比如 query 绑定了 Query()、Preload()、AfterQuery() 方法,其中 Query() 是核心方法,Preload() 实现预加载,AfterQuery() 类似一种 Hook 机制。
小结
责任链模式就是给请求创建一个对象链,链上的对象依次处理同一个请求,每个对象承担各自的处理责任。
Gin 中间件和 GORM 增删改查都涉及到了这种模式的使用。
这种模式有利于解耦请求对象和多个处理对象,处理对象只负责单一职责的功能,如果需要增加功能添加处理对象就行,扩展性好,便于维护。