go责任链核心是函数链而非结构体继承,典型做法为func(context.context, interface{}) (interface{}, error)类型处理器,通过闭包组合、显式调用next控制流程,中断靠返回非nil错误或nil,nil,且须检查ctx.done()响应取消。

责任链的 Go 实现核心:用函数类型串联 Handler
Go 里没有抽象类或接口继承的“标准链式写法”,硬套 Java 那套容易写出冗余结构。真正轻量、符合 Go 习惯的做法,是把每个处理步骤定义为 func(context.Context, interface{}) (interface{}, error) 类型,再用闭包或结构体组合它们。
关键不是“链”本身,而是让上一个 handler 的输出能自然成为下一个的输入——所以统一入参/出参类型比强行实现 setNext() 更重要。
- 避免用指针接收器去实现
Handle()方法,除非你真需要修改 receiver 状态;多数场景下纯函数更清晰 - 如果 handler 需要访问配置或依赖(比如数据库连接),用闭包捕获比塞进 struct 字段更直接
- 不要在链中 panic,统一用
error返回,否则recover()会破坏链的可控性
中间件式链:基于 http.Handler 的实际封装
Web 请求是最常见的责任链使用场景,但直接操作 http.Handler 容易漏掉 context 传递或 response 写入控制。推荐用包装函数方式串起逻辑:
func WithAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isValidToken(r.Header.Get("Authorization")) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func WithLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("req: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
// 使用:handler := WithLogging(WithAuth(realHandler))
这种写法不侵入业务 handler,且顺序明确——越靠外的 wrapper 越早执行(WithLogging 包裹了 WithAuth,所以日志能看到鉴权前的请求)。
- 注意 wrapper 中调用
next.ServeHTTP()前后都可以加逻辑,比如记录耗时要在调用后读取时间差 - 如果某个环节要中断链(如鉴权失败),必须显式 return,不能只写
http.Error() - 所有 wrapper 共享同一个
http.ResponseWriter,但若需拦截响应体(如压缩、加 header),得用ResponseWriter包装器,而非简单透传
非 HTTP 场景:用结构体 + 方法链组织业务逻辑
当处理对象不是 HTTP 请求,而是内部命令、事件或 DTO 时,用结构体封装链更利于测试和扩展。例如订单创建流程:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type OrderProcessor struct {
handlers []func(*Order) error
}
func (p *OrderProcessor) Use(h func(*Order) error) *OrderProcessor {
p.handlers = append(p.handlers, h)
return p
}
func (p *OrderProcessor) Process(o *Order) error {
for _, h := range p.handlers {
if err := h(o); err != nil {
return err
}
}
return nil
}
// 使用:
// p := &OrderProcessor{}
// p.Use(validateOrder).Use(reserveInventory).Use(chargePayment).Process(order)
这种方式比函数链更容易注入 mock handler 做单元测试,也方便在特定位置插入日志或重试逻辑。
- 别在
Use()里做 handler 执行,只负责注册;执行时机由Process()统一控制 - handler 函数签名尽量保持一致,避免混用
*Order和Order,否则修改字段时容易漏掉指针解引用 - 如果某 handler 可能异步执行(如发 MQ 消息),它应返回
error表示投递失败,而不是启动 goroutine 后立即返回 nil
容易被忽略的 context 传播与超时控制
真实系统里,链中每个环节都该尊重上游传入的 context.Context,尤其是涉及 I/O 的 handler(DB 查询、HTTP 调用)。否则一个环节卡住,整条链就阻塞,还无法被 cancel。
错误示范:db.QueryRow("SELECT ...") 没带 context;正确做法是用 db.QueryRowContext(ctx, ...)。
- 所有 handler 输入参数中必须包含
context.Context,且在调用下游服务前传入该 context - 不要在链中间新建 context(如
context.WithTimeout),除非你明确知道该环节的 SLA;超时应由最外层发起者设定 - 如果链中某个 handler 自行派生子 context(比如加 deadline),记得用
defer cancel(),否则可能泄露 goroutine
责任链真正的复杂点不在串联方式,而在每个节点如何协作地响应取消、处理错误、共享状态——这些细节没对齐,链看起来连上了,实际跑起来还是各自为政。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










