验证函数链必须用 func(context.context, interface{}) error 签名,每个函数只做一件事、失败返回 error、成功返回 nil,通过 context 传递中间数据,用切片组合处理器并显式中断,响应改写需用包装器而非替换 responsewriter。

验证函数链必须用 func(context.Context, interface{}) error 签名
Go 里验证逻辑不是“写一堆 if else”,而是让每个校验节点返回 error 表示失败、nil 表示通过并继续——这是最轻量、最易组合的契约。别用 bool 返回值,它没法携带错误原因;也别用结构体封装结果,那会强迫调用方做类型断言。
所有验证函数共享同一签名:type ValidatorFunc func(ctx context.Context, req interface{}) error
这样既能统一调度,又支持闭包注入依赖(比如 DB 客户端、配置项),还不绑定 HTTP 或 RPC 场景。
- 每个函数只做一件事:token 校验、字段非空、权限检查、频率限制……互不耦合
- 失败时直接
return errors.New("missing user_id"),不调用下游 - 成功时必须返回
nil,否则链会提前终止 - 若需传递中间数据(如解析出的用户 ID),用
ctx = context.WithValue(ctx, userIDKey{}, userID),下游通过ctx.Value()取
链式组装用切片而非嵌套结构体
用 []ValidatorFunc 存处理器,比手写 AuthHandler{next: &RateLimitHandler{}} 更可靠。切片天然支持运行时增删、条件跳过、顺序调试,且不会因 nil 指针导致 panic。
构建链的典型方式是:func NewChain(handlers ...ValidatorFunc) *Chain { return &Chain{handlers: handlers} }func (c *Chain) Use(h ValidatorFunc) *Chain { c.handlers = append(c.handlers, h); return c }
- 注册顺序 = 执行顺序:先
Use(RequireToken),再Use(ValidateUser),最后Use(CheckPermission) - 开发环境想跳过限流?直接
if env != "prod" { chain.Remove(RateLimit) } - 调试时加一行
log.Printf("running %v", reflect.TypeOf(h).Name())就能看清哪步卡住 - 别在结构体里存
next字段——每个请求生命周期独立,不需要跨请求维持引用
中断必须显式 return,不能靠“漏调 next”
常见错误是写了 http.Error(w, "forbidden", 403) 却忘了 return,结果后续验证还继续跑,甚至业务 handler 也被执行。Go 没有自动中断机制,控制权全在你手里。
正确做法是在每个验证函数末尾加判断:if err := h(ctx, req); err != nil { return err }
或者更直白地:
err := validateToken(ctx, req)<br>if err != nil { return err }
- 返回非
nilerror 就等于“链断了”,后续函数不会被执行 - 不要用
panic替代 error 返回——它会炸掉整个 goroutine,掩盖本该由业务层处理的语义化错误(如 401、429) - 如果某验证需要异步执行(比如查远端服务),确保 goroutine 内部也用
defer recover(),否则 panic 会逃逸出链 - 所有验证函数都应接受
ctx并监听ctx.Done(),避免超时后还在做无用校验
响应改写要靠 ResponseWriter 包装器,不是替换
验证链本身不负责写 HTTP 响应,但常需在失败时统一封装错误格式(如 {"code":400,"msg":"invalid email"})。这时不能直接替换 http.ResponseWriter,而要用装饰器包装。
定义一个 responseWriterWrapper 结构体,内嵌原始 http.ResponseWriter,重写 WriteHeader() 和 Write() 方法,并记录是否已写出状态:
type responseWriterWrapper struct {
w http.ResponseWriter
written bool
}
func (w *responseWriterWrapper) WriteHeader(statusCode int) {
if !w.written {
w.written = true
w.w.WriteHeader(statusCode)
}
}
- 一旦
WriteHeader()被调用,就不能再改状态码,否则触发http: multiple response.WriteHeader calls - 验证失败时,用
w.Write([]byte(`{"code":401,"msg":"unauthorized"}`)),而不是http.Error()—— 后者会绕过你的包装逻辑 - 包装器必须在链最外层创建(比如
Recovery之后、Logging之前),否则中间件可能直接调用原始WriteHeader - 别把包装器塞进结构体字段里——每个请求新建一个实例,避免状态污染
written 状态的严格检查——这些细节不报错,但会让问题在高并发下才暴露。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











