Go语言设计模式之责任链模式

其实很多人不知道,责任链模式是我们工作中经常遇到的模式,特别是web后端工程师,我们工作中每时每刻都在用:因为市面上大部分的web框架的过滤器基本都是基于这个设计模式为基本模式搭建的。

1.模式介绍

我们先来看一下责任链模式(Chain Of Responsibility Design Pattern )的英文介绍: 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.

翻译成中文就是:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

这么说比较抽象,用更加容易理解的话来进一步解读一下。在责任链模式中,一个请求过来,会有多个处理器(也就是刚刚定义中说的"接收对象")依次处理同一个请求。即请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作责任链模式。

2.模式demo

2.1 UML

责任链模式(Chain Of Responsibility Design Pattern )的整体结构如下:

2.2 标准demo

我们依据标准的UML图,写出一个具体的例子(对应UML图):

首先定义一个接口IHandler

scss 复制代码
type IHandler interface {
	SetNext(handler IHandler)
	Handle(score int)
}

然后分别构建三个不同的实现: ConcreteHandler1

go 复制代码
type ConcreteHandler1 struct {
	Next IHandler
}

func (c *ConcreteHandler1) Handle(score int) {
	if score < 0 {
		fmt.Println("ConcreteHandler1 处理")
		return
	}
	if c.Next != nil {
		c.Next.Handle(score)
	}
	return
}
func (c *ConcreteHandler1) SetNext(handler IHandler) {
	c.Next = handler
}

ConcreteHandler2

go 复制代码
type ConcreteHandler2 struct {
	Next IHandler
}

func (c *ConcreteHandler2) Handle(score int) {
	if score > 0 {
		fmt.Println("ConcreteHandler2 处理")
		return
	}
	if c.Next != nil {
		c.Next.Handle(score)
	}
	return
}

func (c *ConcreteHandler2) SetNext(handler IHandler) {
	c.Next = handler
}

ConcreteHandler3

go 复制代码
type ConcreteHandler3 struct {
	Next IHandler
}

func (c *ConcreteHandler3) Handle(score int) {
	if score == 0 {
		fmt.Println("ConcreteHandler3 处理")
		return
	}
	if c.Next != nil {
		c.Next.Handle(score)
	}
	return
}

func (c *ConcreteHandler3) SetNext(handler IHandler) {
	c.Next = handler
}

最后是main函数:

go 复制代码
func main() {
	handler1 := &ConcreteHandler1{}
	handler2 := &ConcreteHandler2{}
	handler3 := &ConcreteHandler3{}

	handler1.SetNext(handler2)
	handler2.SetNext(handler3)

	handler1.Handle(10)

}

打印结果为:

shell 复制代码
ConcreteHandler2 处理

2.3 改进版demo

通过以上标准例子不难发现:main函数承接了很多client自身之外的"额外工作":构建和拼接组装责任链,这不利于后续client端的使用和扩展:一不小心可能责任链拼就接错了或者拼接少节点了。 我们可以对UML做一个改进:增加一个节点管理模块。改进图如下:

对比上文的uml图,新增加了一个ChainHandler结构体用来管理拼接的Handler,client端无需了解Handler的业务,Handler的组合可以使用链表,也可以使用数组(当前用了数组)。 具体实现如下: 先定义Handler接口:

go 复制代码
type Handler interface {
	Handle(score int)
}

然后分别实现Handler接口的三个结构体: ConcreteHandlerOne

go 复制代码
type ConcreteHandlerOne struct {
	Handler
}

func (c *ConcreteHandlerOne) Handle(score int) {
	if score < 0 {
		fmt.Println("ConcreteHandler1 处理")
		return
	}
}

ConcreteHandlerTwo

go 复制代码
type ConcreteHandlerTwo struct {
	Handler
}

func (c *ConcreteHandlerTwo) Handle(score int) {
	if score > 0 {
		fmt.Println("ConcreteHandler2 处理")
		return
	}
}

ConcreteHandlerThree

go 复制代码
type ConcreteHandlerThree struct {
	Handler
}

func (c *ConcreteHandlerThree) Handle(score int) {
	if score == 0 {
		fmt.Println("ConcreteHandler3 处理")
		return
	}
}

main函数调用(client调用):

scss 复制代码
func main() {
	chain := &ChainHandler{}
	chain.AddHandler(&ConcreteHandlerOne{})
	chain.AddHandler(&ConcreteHandlerTwo{})
	chain.AddHandler(&ConcreteHandlerThree{})
	chain.Handle(10)
}

最终的实现结构图:

日常工作中出现的责任链模式(Chain Of Responsibility Design Pattern )一般都是以上这种包含Hanlder管理的模式。

3. 源码解析

在日常框架和语言基础库中,经常能够看到很多场景使用了责任链模式。

3.1 beego过滤器

可以对比改进版demo的uml图,beego的过滤器就是按照这种模式来设计的(当前参照的beego版本是2.0)。

3.1.1 client端

调用端首先是过滤器的注册:

vbscript 复制代码
web.InsertFilter("/v2/api/*", web.BeforeRouter, auth.AuthAPIFilter)

然后在github.com/beego/beego/v2@v2.0.3/server/web/router.goControllerRegister结构体的serveHttp函数中

Go 复制代码
if len(p.filters[BeforeRouter]) > 0 && p.execFilter(ctx, urlPath, BeforeRouter) {
		goto Admin
}

以上 p.execFilter(ctx, urlPath, BeforeRouter)处,启动调用。

3.1.2 Handler接口

Handler接口很简单

go 复制代码
// HandleFunc define how to process the request
type HandleFunc func(ctx *beecontext.Context)

	...
	
type FilterFunc = HandleFunc

3.1.3 Handler接口实现

接口的实现扩展比较灵活,直接把用户定义的函数作为接口的实现。与client端中的过滤器注册联动。

scss 复制代码
// 过滤器注册
web.InsertFilter("/v2/api/*", web.BeforeRouter, auth.AuthAPIFilter)

// 自定义过滤器
var AuthAPIFilter = func(ctx *context.Context) {
	isAccess := validateAccess(ctx)
	if !isAccess {
		res, _ := json.Marshal(r)
		ctx.WriteString(string(res))
		// ctx.Redirect(401, "/401")
	}
}

3.1.4 Handler管理

Handler的管理模块是在github.com/beego/beego/v2@v2.0.3/server/web/router.go的中的 FilterRouterControllerRegister两个结构体中

go 复制代码
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
	routers      map[string]*Tree
	enablePolicy bool
	enableFilter bool
	policies     map[string]*Tree
	filters      [FinishRouter + 1][]*FilterRouter
	pool         sync.Pool

	// the filter created by FilterChain
	chainRoot *FilterRouter

	// keep registered chain and build it when serve http
	filterChains []filterChainConfig

	cfg *Config
}


type FilterRouter struct {
	filterFunc     FilterFunc
	next           *FilterRouter
	tree           *Tree
	pattern        string
	returnOnOutput bool
	resetParams    bool
}

FilterRouter是一个链表,包含用户自定义的过滤函数;ControllerRegisterFilterRouter进行管理。

3.2 Go源码http.handler

我们在使用Go构建http web服务器的时候,使用的http.Handler就是使用的责任链模式。

go 复制代码
package main

import (
	"net/http"
)

func main() {
	s := http.NewServeMux()

	s.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {

		// todo ....

		return
	})

	http.ListenAndServe(":80", s)

}

2.3 的UML图为标准,整体的对照结构图如下:

3.2.1 client端

整个模式的启动是随着http server启动后,接受到请求后的处理开始的。在net/http/server.goserve函数中

scss 复制代码
func (c *conn) serve(ctx context.Context) {
	...
	
	// HTTP cannot have multiple simultaneous active requests.[*]
	// Until the server replies to this request, it can't read another,
	// so we might as well run the handler in this goroutine.
	// [*] Not strictly true: HTTP pipelining. We could let them all process
	// in parallel even if their responses need to be serialized.
	// But we're not going to implement HTTP pipelining because it
	// was never deployed in the wild and the answer is HTTP/2.
	serverHandler{c.server}.ServeHTTP(w, w.req)
	
	...

}

可以看到http server的原理很简单,就是for 死循环等待接收,然后一个请求过来,就对应的生成一个单独的协程goroutine去处理。

3.2.2 Handler接口

Go源码中对责任链模式的实现非常标准,Handler接口与设计模式中的Handler接口同名,在net/http/server.go中:

go 复制代码
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

为了扩展方便,在使用过程中并非直接使用,而是中间又加了一层抽象层(相当于Java中的抽象类了,Go中没有抽象类)

scss 复制代码
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

3.2.3 Handler接口实现

与上文中提到的Beego的过滤器类似,Go的Handler设计的也非常容易扩展,用户自定义的请求处理函数Handler都会变成Handler的子类。

go 复制代码
func main() {
	s := http.NewServeMux()

	s.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {

		// todo ....

		return
	})

	http.ListenAndServe(":80", s)

}

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	// 强制类型转换,转成了实现了Hanlder的"抽象类"HandlerFunc
	mux.Handle(pattern, HandlerFunc(handler)) 
	
}

注意看上文的HandleFunc中的 mux.Handle(pattern, HandlerFunc(handler)) 这一行,将用户自定义的处理函数强制转换成了上文3.2.2中的Handler的"抽象类"HandlerFunc类型,进而实现了继承。

3.2.4 Handler接口的管理类ChainHandler

Go中对Handler的管理类是在net/http/server.go文件的ServeMux结构体和muxEntry结构体中:

go 复制代码
type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

type muxEntry struct {
	h       Handler
	pattern string
}

其中,用户自定以的处理函数都被封装到了muxEntry结构体的Handler中,一个自定义的函数对应一个muxEntryServeMux使用hashmap对muxEntry集合进行管理(上文的beego中是使用的链表,上文demo中使用了数组)。当web server接收到请求的时候,ServeMux会根据hashmap找到相应的handler然后处理。

go 复制代码
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
    
    // *******寻找handler*******
	h, _ := mux.Handler(r)
	
	h.ServeHTTP(w, r)
}

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	...

	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
		return RedirectHandler(u.String(), StatusMovedPermanently), pattern
	}

	// *******寻找handler*******
	return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// Host-specific pattern takes precedence over generic ones
	if mux.hosts {
	    // *******寻找handler*******
		h, pattern = mux.match(host + path)
	}
	if h == nil {
	    // *******寻找handler*******
		h, pattern = mux.match(path)
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}


func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	
	// ********通过hashmap找到相关handler*********
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

在程序运行过程中,用户注册自定义的函数被转化成了Handler,然后Handler又结合用户自定义的URL地址被ServeMuxURL为Key、Handler为Value做成hashmap管理起来;等到请求来的时候,ServeMux就根据用户请求的URL地址,从hashmap中找到具体的Hanlder来处理请求。

4. 总结

责任链模式的基本思想就是要处理的请求(通常会是结构体,然后作为函数参数);依次经过多个处理对象处理,这些处理函数可以动态的添加和删除,具备很高的灵活性和扩展性,通常会对这些处理函数做统一处理,存储方式一般是通过链表、数组、hash map等存储结构。

责任链模式的应用非常广泛:

  1. 业务场景:作为敏感词(涉黄、政治、反动等此)过滤的设计结构
  2. 技术框架:路由、router过滤器、日志log框架等等
相关推荐
2601_957884845 小时前
深度拆解:大模型RAG架构下,GEO优化的技术实现路径
人工智能·架构
㳺三才人子5 小时前
初探 Flask
后端·python·flask·html
星栈独行5 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.6 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl7 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
Curvatureflight7 小时前
【架构实战】生产级大模型 API 接入指南:流式响应(Streaming)异常处理与监控闭环
python·架构
这是谁的博客?8 小时前
微服务架构设计模式深度解析:从拆分策略到容灾机制
微服务·设计模式·云原生·架构·架构设计·后端开发·分布式系统
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端