Go Web 开发提速 3(gos):Filter 实战与变量注入 —— 通用逻辑复用与依赖解耦

在前两篇内容中,我们已经搭建了 gos 的基础使用框架:从「# Go Web 开发提速1(gos):基于 Spring 式注释方案,用 gos 自动生成运行代码」到「# Go Web 开发提速2 (gos):Servlet 注解与参数解析全指南 ------ 从定义到落地」,解决了 URL 绑定、参数映射等重复劳动问题。今天我们进入进阶环节 ------ 聚焦通用逻辑复用 (Filter 过滤器)与依赖自动管理(变量注入),让鉴权、日志等跨接口逻辑不用重复写,数据库连接、配置等依赖不用手动初始化,进一步提升开发效率。

一、Filter 过滤器:让通用逻辑 "一次定义,多处复用"

在 Web 开发中,日志记录、Token 鉴权,跨域处理等逻辑往往需要在多个接口中重复编写。gos 的 Filter 设计借鉴了,Spring 拦截器思想,通过注释定义规则,实现 "通用逻辑抽离 + 按需生效",无需侵入业务代码。

1. Filter 的核心定义规则

要创建一个 Filter,需通过注释参数声明其范围,再按固定签名定义函数。

(1)Filter 注释参数

Filter 的注释格式为:// @gos type=filter; [group=分组名]; [url=匹配规则]

各参数含义与使用场景如下:

参数名 必填 说明 示例
type=filter 声明该函数 / 结构体为 Filter 组件,gos 会自动扫描并注册 基础必填参数,无它则不被识别为 Filter
group 指定 Filter 生效的服务分组(与 Servlet 的 group 对应),仅对该分组的接口生效 group=user 表示仅对 user 分组(如 8080 端口)的接口生效
url URL 匹配规则:- 为空字符串(url=""):全局生效,匹配所有接口- 非空字符串:匹配 "URL 包含该字符串" 的接口 url="/tk/" 表示匹配所有 URL 含 /tk/ 的接口

(2)Filter 函数签名规范

Filter 函数需严格遵循以下签名,确保 gos 能正确生成调用逻辑:

go 复制代码
func 函数名(c filterContext, prequest **http.Request) error

两个参数的设计细节与使用场景,是 gos 解耦框架依赖、提升灵活性的关键: * 第一个参数: filterContext (上下文)

go 复制代码
表面是自定义接口,实际运行时传入的是 `gin.Context`,但通过接口抽象屏蔽了 gin 的依赖 ------ 用户只需依赖 `filterContext` 定义的方法,无需在业务代码中引入 gin 包。

推荐的 `filterContext` 定义(仅保留核心能力,避免过度依赖):
// 自定义 filterContext,仅暴露必要方法
type filterContext interface {
    context.Context  // 继承上下文基础能力
    Next()           // 执行下一个 Filter/业务逻辑(核心:不调用则中断流程)
    // 可按需扩展:如 Set、Get 存储临时数据,或 WriteHeader 直接返回响应
}

优势:后续若切换 Web 框架(如从 gin 到 echo),Filter 代码无需修改,只需适配 filterContext 接口即可。

  • 第二个参数: **http.Request (双指针请求对象) 采用 "指向 http.Request 的指针",核心目的是允许 Filter 修改请求对象本身(而非副本)。例如:

    • 在鉴权 Filter 中添加请求头((*prequest).Header.Set("X-User-ID", "123"));

    • 修正请求参数(如统一编码 URL 中的特殊字符)。

      若用单指针(*http.Request),修改的是函数参数副本的指向,无法影响原始请求 ------ 双指针完美解决了这个问题。

2. Filter 的两种生效方式:自动匹配 vs 手动指定

gos 提供两种 Filter 绑定逻辑,覆盖 "批量生效" 和 "精准控制" 场景,无需手动编写注册代码。

(1)自动匹配:按 URL 规则批量生效

适用于 "某类接口需统一执行某逻辑" 的场景(如所有鉴权接口走 Token 校验)。

核心逻辑 :URL 包含 Filter 注释中 url 字段的接口,会自动触发该 Filter。

实战示例:Token 鉴权 Filter

  1. 定义 CheckToken Filter(URL 含 /tk/ 的接口自动生效):
go 复制代码
// @gos type=filter; url="/tk/"; title="Token 鉴权过滤器"
func CheckToken(c filterContext, prequest **http.Request) error {
    // 1. 从请求头获取 Token
    token := (*prequest).Header.Get("Authorization")
    if token == "" {
        // 鉴权失败:直接返回错误,中断后续流程(不调用 c.Next())
        return fmt.Errorf("401: 未携带 Token")
    }
    // 2. 校验 Token(模拟逻辑:此处省略 JWT 等真实校验)
    if !isValidToken(token) {
        return fmt.Errorf("403: Token 无效")
    }
    // 3. 鉴权成功:执行下一个 Filter/业务逻辑
    c.Next()
    return nil

}

// 模拟 Token 校验函数
func isValidToken(token string) bool {
    return token == "valid_token_123" // 实际场景替换为真实校验逻辑
}
  1. 定义需鉴权的 Servlet(URL 含 /tk/):
go 复制代码
// @gos type=servlet; group=/user
type StudentServlet struct{}
//  URL 含 /tk/ → 自动触发 CheckToken Filter
// @gos url=/tk/student/getName; title="获取学生姓名(需鉴权)"
func (s *StudentServlet) GetStudentName(ctx context.Context, req *GetStudentNameReq) (string, error) {
    return "张三", nil
}
  1. 测试效果:

  2. 未带 Token 请求:返回 {"code":401,"msg":"未携带 Token","obj":null}(流程中断,未执行业务函数);

  3. 带有效 Token 请求:正常返回 {"code":0,"msg":"success","obj":"张三"}(先执行 Filter,再执行业务)。

(2)手动指定:按函数名精准绑定

适用于 "部分接口需特殊组合 Filter" 的场景(如某接口需同时鉴权 + 日志记录)。 核心逻辑 :在 Servlet 注释中通过 filters=函数名1,函数名2 指定 Filter,执行顺序与逗号分隔顺序一致。

实战示例:多 Filter 组合使用

  1. 先定义一个日志 Filter(记录请求耗时):
go 复制代码
// @gos type=filter; title="请求耗时日志过滤器"
func LogCostTime(c filterContext, prequest **http.Request) error {
    start := time.Now()
    // 先执行后续逻辑(Filter/业务)
    c.Next()
    // 后续逻辑执行完后,计算耗时
    cost := time.Since(start)
    fmt.Printf("请求 %s 耗时:%v\n", (*prequest).URL.Path, cost)
    return nil

}
  1. 在 Servlet 中手动指定 "鉴权 + 日志" 两个 Filter:
go 复制代码
// @gos url=/tk/student/getScore; method=GET; 
// @gos title="获取学生成绩(需鉴权+日志)"; filters=CheckToken,LogCostTime
func (s *StudentServlet) GetStudentScore(ctx context.Context, req *GetStudentScoreReq) (int, error) {
    return 95, nil
}
  1. 执行顺序:CheckTokenLogCostTime → 业务函数 (注:若 CheckToken 鉴权失败,LogCostTime 和业务函数都不会执行,符合 "中断逻辑" 预期)。

二、变量注入:让依赖管理 "自动生成,无需手动"

在传统 Go 开发中,数据库连接、配置对象等依赖需要手动初始化并传递,不仅重复,还容易出现 "依赖顺序错误"(如未初始化 DB 就先使用)。gos 的变量注入通过 "注释声明依赖 + 自动生成初始化代码",解决了这两个痛点。 核心逻辑分为两步:1. 定义变量生成规则(注入源)2. 声明变量使用场景(注入目标) ,gos 会自动解析依赖链,按顺序生成代码。

1. 第一步:定义注入源(变量生成方式)

gos 支持两种注入源:initiator 函数(通过函数生成变量)和 autogen 结构体(通过结构体生成变量)。

(1)通过 initiator 函数生成(灵活度高)

给函数添加 // @gos type=initiator 注释,函数的返回值会作为 "可注入的变量"。支持两种返回值形式: 带变量名的返回值:便于后续按名称匹配注入; 无变量名的返回值:会作为该类型的 "默认注入源"(同一类型只能有一个)。

实战示例:生成数据库连接与配置

go 复制代码
// 1. 生成配置对象(带返回值名称,便于按名注入)
// @gos type=initiator; title="生成配置对象"
func InitConfig() (config *Config, err error) {
    // 模拟从配置文件读取
    return &Config{
        DBHost: "localhost",
        DBPort: 3306,
        LogDBHost: "logDbhost",
        LogDBPort: 3306,
    }, nil
}

// 2. 生成数据库连接(依赖 Config,带返回值名称)
// @gos type=initiator; title="生成用户数据库连接"
func InitDB1(config *Config) (userDb *sql.DB) {
    // 依赖注入:参数 config 会自动从 InitConfig 生成的变量中获取
    dsn := fmt.Sprintf("%s:%d", config.DBHost, config.DBPort)
    var err error
    userDb,err = sql.Open("mysql", dsn)
    if err!=nil {
        panic(***)
    }
}

// @gos type=initiator; title="生成日志数据库连接"
func InitDB2(config *Config) (logDb *sql.DB, err error) {
    // 依赖注入:参数 config 会自动从 InitConfig 生成的变量中获取
    dsn := fmt.Sprintf("%s:%d", config.LogDBHost, config.LogDBPort)
    var err error
    logDb,err = sql.Open("mysql", dsn)
    if err!=nil {
        panic(***)
    }
}

// 3. 生成默认 Redis 连接(无返回值名称,作为 *redis.Client 类型的默认源)
// @gos type=initiator; title="生成默认 Redis 连接"
func InitDefaultRedis() (*redis.Client, error) {
    return redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    }), nil
}

// 自定义类型
type Config struct {
    DBHost string
    DBPort int
}

(2)通过 autogen 结构体生成(简化结构体初始化)

  • 给结构体添加 // @gos type=autogen 注释,gos 会自动生成该结构体的实例变量,并对其属性进行注入(仅注入结构体类型,原生类型需指定默认值)。
  • http服务的结构体自动有autogen属性;
  • 如果http服务器的机构体被initiator函数返回时,其自定义autogen取消;

实战示例:自动生成带依赖的服务结构体

go 复制代码
// @gos autogen
type StudentBiz struct {
    // 结构体类型:自动从注入源中匹配(此处匹配 InitDB1 生成的 userDb)
    UserDb *sql.DB 
}

// servlet服务器的结构体自带autogen属性;
// @gos type=servlet; group=student; title="自动生成 UserService 实例"
type UserService struct {

    // 原生类型:通过 default 标签设置初始值,不注入
    MaxPageSize int `default:"20"`   

    // 跳过注入:通过 wire:"-" 标签排除
    TempData string `wire:"-"`

    // 自动注入StudentBiz的对象。 StudentBiz有autogen定义;
    StudentBiz *StudentBiz;
}

// @gos type=servlet; url="/tk/student/getName"
func (userService *UserService) GetName(ctx context.Context,req *NameReq)(string,error){

}

生成逻辑:gos 会生成 StudentBiz 对象,自动注入 UserDb(从 InitDB1 的返回值获取),生成 UserService 对象,自动注入 StudentBiz 对象,给 MaxPageSize 赋值 20,且忽略 TempData 的注入。

2. 第二步:变量注入的使用场景

注入源定义后,有两种核心使用场景,均无需手动传递依赖 ------gos 会自动匹配。

(1)场景 1:initiator 函数依赖注入

如前面 InitDB1 函数的参数 config *Config,gos 会自动查找 *Config 类型的注入源(InitConfig 生成的 config),先执行 InitConfig,并保存其返回值,再将保存的返回值为 InitDB1 的参数,确保依赖顺序正确。

(2)场景 2:结构体自动注入

autogen 结构体生成实例时,会自动对其 "非原生类型、未加 wire:"-"" 的属性进行注入,如 UserServiceUserDb *sql.DB 会自动匹配 InitDB1 生成的 db

3. 变量注入的核心规则(避免匹配冲突)

当存在多个同类型注入源时,gos 按以下优先级匹配,确保注入准确性: 1. 类型必须相同 :匹配与注入目标类型完全一致的注入源(如 *Config 类型的参数,只找 *Config 类型的注入源),如果找不到,则会报错;

  1. 名称匹配 :若同类型有多个注入源,按 "注入目标名称" 与 "注入源返回值名称" 匹配(如参数名或者属性名 userDb 匹配返回值名 userDb*mysql.DB 注入源,也就是InitDB1的返回值,而不是InitDB2的返回值);

  2. 默认值兜底

  3. 若注入源无返回值名称(如 InitDefaultRedis() 返回 (*redis.Client, error)),则作为该类型的默认源;

  4. 若同类型只有一个注入源(即使带返回值名称),也会作为默认源(如 *Config 只有 InitConfig 一个注入源,即使返回值名是 config,也会给所有 *Config 参数注入)。

4. 进阶说明:依赖链与特殊场景

(1)依赖链自动解析

gos 会扫描所有注入源的依赖关系,生成 "拓扑排序后的初始化顺序"。例如:

依赖顺序:InitDB1 依赖 InitConfigStudentBiz 依赖 InitDB1UserService 依赖 StudentBiz

生成顺序:InitConfigInitDB1StudentBizUserService,完全无需手动控制顺序。

(2)无返回值的 initiator 函数

initiator 函数允许无返回值,可用于 "需要自动执行但无需生成变量" 的场景(如初始化日志组件、注册驱动,协程启动等):

c 复制代码
// @gos type=initiator; title="初始化日志组件"

func InitLogger() {

    // 初始化日志配置,无返回值但会自动执行

    log.SetOutput(os.Stdout)

    log.SetPrefix("[gos] ")

}

(3)结构体注入的细节补充

  • 原生类型初始化 :仅支持通过 default 标签设置初始值,不支持注入(如 MaxPageSize intdefault:"20" 赋值);

  • 跳过注入wire:"-" 标签不仅适用于 autogen 结构体,也适用于 initiator 函数的参数(若某参数不想注入,可加该标签,但需手动传值,较少用);

三、总结与下期预告

本篇我们掌握了 gos 两个核心进阶能力:

  1. Filter 过滤器 :通过 url 自动匹配和 filters 手动指定,实现鉴权、日志等通用逻辑的复用,同时通过 filterContext 解耦框架依赖;

  2. 变量注入 :通过 initiator 函数和 autogen 结构体定义注入源,按 "类型→名称→默认值" 规则自动匹配,解决依赖初始化重复与顺序问题。

有了 Filter 和变量注入,我们的 Web 服务不仅能减少重复代码,还能保证通用逻辑的一致性与依赖的稳定性。下一篇,为了让用户对自动生成机制有更直接的了解,我们一起看看自动生成的代码,让她不再神秘;

如果在 Filter 定义或变量注入中遇到匹配冲突、依赖解析失败等问题,欢迎在评论区留言讨论!

相关推荐
rannn_1111 小时前
【Redis|原理篇2】Redis网络模型、通信协议、内存回收
java·网络·redis·后端·缓存
RDCJM2 小时前
Springboot的jak安装与配置教程
java·spring boot·后端
_Evan_Yao3 小时前
对话的边界:HTTP 的克制,SSE 的流淌,WebSocket 的自由
java·后端·websocket·网络协议·http
危桥带雨3 小时前
FLASH代码部分
java·后端·spring
Rust研习社3 小时前
添加依赖库时的 features 是什么?优雅实现编译期条件编译与模块化开发
开发语言·后端·rust
马艳泽4 小时前
接到新需求后快速产出可执行的方案和时间估算
后端
Rust研习社4 小时前
Rust 条件变量(Condvar)详解:线程同步的高效方式
后端·rust·编程语言
fliter4 小时前
用逆波兰表达式,彻底搞懂 Rust 宏的递归写法
后端
fliter4 小时前
不开端口,不配 DNS,用树莓派在家搭一个公网可访问的 Web 服务
后端