在前两篇内容中,我们已经搭建了 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
- 定义 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" // 实际场景替换为真实校验逻辑
}
- 定义需鉴权的 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
}
-
测试效果:
-
未带 Token 请求:返回
{"code":401,"msg":"未携带 Token","obj":null}(流程中断,未执行业务函数); -
带有效 Token 请求:正常返回
{"code":0,"msg":"success","obj":"张三"}(先执行 Filter,再执行业务)。
(2)手动指定:按函数名精准绑定
适用于 "部分接口需特殊组合 Filter" 的场景(如某接口需同时鉴权 + 日志记录)。 核心逻辑 :在 Servlet 注释中通过 filters=函数名1,函数名2 指定 Filter,执行顺序与逗号分隔顺序一致。
实战示例:多 Filter 组合使用
- 先定义一个日志 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
}
- 在 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
}
- 执行顺序:
CheckToken→LogCostTime→ 业务函数 (注:若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:"-"" 的属性进行注入,如 UserService 的 UserDb *sql.DB 会自动匹配 InitDB1 生成的 db。
3. 变量注入的核心规则(避免匹配冲突)
当存在多个同类型注入源时,gos 按以下优先级匹配,确保注入准确性: 1. 类型必须相同 :匹配与注入目标类型完全一致的注入源(如 *Config 类型的参数,只找 *Config 类型的注入源),如果找不到,则会报错;
-
名称匹配 :若同类型有多个注入源,按 "注入目标名称" 与 "注入源返回值名称" 匹配(如参数名或者属性名
userDb匹配返回值名userDb的*mysql.DB注入源,也就是InitDB1的返回值,而不是InitDB2的返回值); -
默认值兜底:
-
若注入源无返回值名称(如
InitDefaultRedis()返回(*redis.Client, error)),则作为该类型的默认源; -
若同类型只有一个注入源(即使带返回值名称),也会作为默认源(如
*Config只有InitConfig一个注入源,即使返回值名是config,也会给所有*Config参数注入)。
4. 进阶说明:依赖链与特殊场景
(1)依赖链自动解析
gos 会扫描所有注入源的依赖关系,生成 "拓扑排序后的初始化顺序"。例如:
依赖顺序:InitDB1 依赖 InitConfig → StudentBiz 依赖 InitDB1→ UserService 依赖 StudentBiz
生成顺序:InitConfig → InitDB1 → StudentBiz → UserService,完全无需手动控制顺序。
(2)无返回值的 initiator 函数
initiator 函数允许无返回值,可用于 "需要自动执行但无需生成变量" 的场景(如初始化日志组件、注册驱动,协程启动等):
c
// @gos type=initiator; title="初始化日志组件"
func InitLogger() {
// 初始化日志配置,无返回值但会自动执行
log.SetOutput(os.Stdout)
log.SetPrefix("[gos] ")
}
(3)结构体注入的细节补充
-
原生类型初始化 :仅支持通过
default标签设置初始值,不支持注入(如MaxPageSize int用default:"20"赋值); -
跳过注入 :
wire:"-"标签不仅适用于autogen结构体,也适用于initiator函数的参数(若某参数不想注入,可加该标签,但需手动传值,较少用);
三、总结与下期预告
本篇我们掌握了 gos 两个核心进阶能力:
-
Filter 过滤器 :通过
url自动匹配和filters手动指定,实现鉴权、日志等通用逻辑的复用,同时通过filterContext解耦框架依赖; -
变量注入 :通过
initiator函数和autogen结构体定义注入源,按 "类型→名称→默认值" 规则自动匹配,解决依赖初始化重复与顺序问题。
有了 Filter 和变量注入,我们的 Web 服务不仅能减少重复代码,还能保证通用逻辑的一致性与依赖的稳定性。下一篇,为了让用户对自动生成机制有更直接的了解,我们一起看看自动生成的代码,让她不再神秘;
如果在 Filter 定义或变量注入中遇到匹配冲突、依赖解析失败等问题,欢迎在评论区留言讨论!