当泛型遇上自引用约束,代码的表达力将迎来质的飞跃
Go 语言的泛型自 1.18 版本引入以来,一直存在一个令人遗憾的限制——泛型类型不能在自身的类型参数列表中引用自己。这意味着诸如"链式调用返回具体子类型"这样在 Java/C# 中司空见惯的模式,在 Go 中只能通过
any类型断言来曲线救国。直到 Go 1.26 的发布,这一限制终于被打破。本文将继续对《Go泛型最佳实践:封装可扩展数据查询构建器》中介绍的查询构建器进行版本迭代,带你深入理解这一特性如何彻底改变架构设计。
Go 1.26 自引用泛型约束:到底解锁了什么?
Go 1.26 版本正式解除了"泛型类型不得在自身类型参数列表中自我引用"的限制。简单来说,以前你不能这样写:
// Go 1.25 及之前:编译错误!
type Builder[B Builder[B]] struct {
// ...
}而在 Go 1.26 中,这段代码完全合法。这意味着我们可以定义这样的接口约束:
type queryBuilder[B any, R any] interface {
self() B
QueryList(ctx context.Context) ([]*R, int64, error)
}
// Go 1.26:合法!
type builder[B queryBuilder[B, R], R any] struct {
data *DBProxy
start uint32
limit uint32
needTotal bool
needPagination bool
middlewares []Middleware[R]
selfRef B // 存储具体子类型引用
querierRef Querier[R] // 存储 Querier 接口引用
}这里的关键在于 B 类型参数——它代表"实现者自身的类型"。当一个具体的构建器(如 GormBuilder)实现这个接口时,B 就是 *GormBuilder[R],这样 self() 方法就能返回具体的子类型,而非笼统的接口类型。
这解决了什么问题? 一句话概括:让基类的方法能够返回子类的具体类型,实现真正类型安全的链式调用。
痛点回顾:旧架构的"类型断言之殇"
在 Go 1.26 之前,我们的查询构建器采用经典的策略模式。核心思路是:一个通用的 builder 结构体持有一个 Strategy 接口,不同数据源(MySQL、MongoDB、Elasticsearch)各自实现该接口。
旧版核心结构
// 旧版:单一构建器 + 策略接口
type builder[R any] struct {
data *DBProxy
start uint32
limit uint32
needTotal bool
needPagination bool
strategy Strategy[R] // 查询策略
middlewares []Middleware[R] // 中间件链
filter func(context.Context) (any, error) // 返回 any!
sort func() any // 返回 any!
}
// 策略接口
type Strategy[R any] interface {
QueryList(context.Context, *builder[R]) ([]*R, int64, error)
}旧版策略实现(以 GORM 为例)
func (s *QueryGormListStrategy[R]) QueryList(
ctx context.Context,
builder *builder[R],
) (list []*R, total int64, err error) {
filterScope, err := builder.filter(ctx)
if err != nil {
return nil, 0, err
}
sortScope := builder.sort()
// 😱 运行时类型断言!如果类型不匹配,直接 panic
for _, scope := range []any{filterScope, sortScope} {
if _, ok := scope.(func(*gorm.DB) *gorm.DB); !ok {
return nil, 0, errors.New("invalid scope")
}
}
// 使用时还需要再次断言
query := builder.data.db.WithContext(ctx).
Model(&list).
Scopes(filterScope.(func(*gorm.DB) *gorm.DB), sortScope.(func(*gorm.DB) *gorm.DB))
// ...
}旧版调用方式
// 旧版 Service 层:filter/sort 返回 any,类型安全全靠"信任"
type Service interface {
GetFilter(context.Context) (any, error) // 返回 any
GetSort() any // 返回 any
}
type BaseService[R any, F any, S any] struct {
builder builder[R]
filter *F
sort S
service Service
}问题一目了然:
filter和sort的类型是any,编译器无法检查你传入的是func(*gorm.DB) *gorm.DB还是bson.D,类型错误只能在运行时暴露- 策略实现中充斥着类型断言,每个策略都要手动验证 filter/sort 的类型,代码冗余且脆弱
- 链式调用只能返回
*builder[R],无法返回具体的子类型,扩展性受限
新架构:自引用泛型约束的优雅重构
Go 1.26 的自引用泛型约束让我们可以彻底重新设计这套架构。核心思想是:将单一构建器 + 策略模式,重构为公共基类 + 专属构建器模式。
1. 自引用泛型基类:架构的基石
package builder
import (
"context"
"fmt"
"github.com/olivere/elastic/v7"
"go.mongodb.org/mongo-driver/mongo"
"gorm.io/gorm"
)
// DataSource 数据源类型枚举
type DataSource int
const (
MySQL DataSource = iota
MongoDB
ElasticSearch
)
// queryBuilder 构建器接口约束,利用 Go 1.26 自引用泛型约束特性
// 泛型参数:
// B: 具体构建器类型(自引用)
// R: 查询结果的实体类型
type queryBuilder[B any, R any] interface {
self() B
QueryList(ctx context.Context) ([]*R, int64, error)
}
// Querier 通用查询接口,作为工厂函数的返回类型
type Querier[R any] interface {
Use(middleware Middleware[R]) Querier[R]
SetStart(start uint32) Querier[R]
SetLimit(limit uint32) Querier[R]
SetNeedTotal(needTotal bool) Querier[R]
SetNeedPagination(needPagination bool) Querier[R]
QueryList(ctx context.Context) ([]*R, int64, error)
}
// builder 查询构建器公共模板基类,使用自引用泛型约束
// 泛型参数:
// B: 具体构建器类型(自引用,满足 queryBuilder 约束)
// R: 查询结果的实体类型
type builder[B queryBuilder[B, R], R any] struct {
data *DBProxy
start uint32
limit uint32
needTotal bool
needPagination bool
middlewares []Middleware[R]
selfRef B // 存储具体子类型引用
querierRef Querier[R] // 存储 Querier 接口引用
}关键设计解读:
builder[B queryBuilder[B, R], R any]中的B就是自引用泛型参数,它约束了B必须实现queryBuilder[B, R]接口selfRef字段存储具体子类型的引用,使得基类方法可以返回子类型querierRef字段存储Querier[R]接口引用,供中间件使用时避免类型断言
2. 基类的链式调用方法
// setSelf 设置具体子类型引用,供子类型构造时调用
func (b *builder[B, R]) setSelf(self B, querier Querier[R]) {
b.selfRef = self
b.querierRef = querier
}
// Use 添加中间件,返回具体子类型
func (b *builder[B, R]) Use(middleware Middleware[R]) B {
b.middlewares = append(b.middlewares, middleware)
return b.selfRef // 返回的是具体子类型,不是 *builder!
}
// SetStart 设置分页起始位置
func (b *builder[B, R]) SetStart(start uint32) B {
b.start = start
return b.selfRef
}
// SetLimit 设置每页数据条数
func (b *builder[B, R]) SetLimit(limit uint32) B {
b.limit = limit
return b.selfRef
}
// SetNeedTotal 设置是否需要查询总数
func (b *builder[B, R]) SetNeedTotal(needTotal bool) B {
b.needTotal = needTotal
return b.selfRef
}
// SetNeedPagination 设置是否需要分页
func (b *builder[B, R]) SetNeedPagination(needPagination bool) B {
b.needPagination = needPagination
return b.selfRef
}注意这里每个方法的返回值都是 B——即具体的子类型。这意味着当你调用 GormBuilder.SetStart(0) 时,返回的是 *GormBuilder[R],而不是 *builder[B, R],链式调用后仍然可以访问 GormBuilder 的专属方法。
3. 中间件执行引擎
// executeWithMiddlewares 执行中间件链并调用最终查询逻辑
func (b *builder[B, R]) executeWithMiddlewares(
ctx context.Context,
queryFn func(context.Context) ([]*R, int64, error),
) ([]*R, int64, error) {
next := queryFn
for i := len(b.middlewares) - 1; i >= 0; i-- {
next = func(mw Middleware[R], fn func(context.Context) ([]*R, int64, error)) func(context.Context) ([]*R, int64, error) {
return func(ctx context.Context) ([]*R, int64, error) {
return mw(ctx, b.querierRef, fn)
}
}(b.middlewares[i], next)
}
return next(ctx)
}中间件签名也随之升级,从旧版的 *builder[R] 变为 Querier[R] 接口:
// 新版中间件签名
type Middleware[R any] func(
ctx context.Context,
builder Querier[R], // 从 *builder[R] 升级为 Querier[R] 接口
next func(context.Context) ([]*R, int64, error),
) ([]*R, int64, error)专属构建器:零类型断言的强类型体验
有了自引用泛型基类,每个数据源都可以拥有自己的专属构建器,各自定义强类型的 SetFilter 和 SetSort 方法。
GormBuilder:MySQL 专属构建器
// GormScope GORM 查询作用域类型
type GormScope = func(*gorm.DB) *gorm.DB
// GormBuilder MySQL(GORM)专属查询构建器
type GormBuilder[R any] struct {
builder[*GormBuilder[R], R] // 自引用:B = *GormBuilder[R]
filter GormScope // 强类型!不再是 any
sort GormScope // 强类型!不再是 any
}
// self 返回自身引用
func (g *GormBuilder[R]) self() *GormBuilder[R] {
return g
}
// NewGormBuilder 创建 GORM 专属查询构建器实例
func NewGormBuilder[R any](data *DBProxy) *GormBuilder[R] {
g := &GormBuilder[R]{}
g.builder.data = data
g.builder.setSelf(g, g) // 关键:将自身引用注入基类
return g
}
// SetFilter 设置 GORM 过滤条件——强类型,编译期检查!
func (g *GormBuilder[R]) SetFilter(filter GormScope) *GormBuilder[R] {
g.filter = filter
return g
}
// SetSort 设置 GORM 排序条件——强类型,编译期检查!
func (g *GormBuilder[R]) SetSort(sort GormScope) *GormBuilder[R] {
g.sort = sort
return g
}
// QueryList 执行 GORM 查询列表操作
func (g *GormBuilder[R]) QueryList(ctx context.Context) ([]*R, int64, error) {
return g.builder.executeWithMiddlewares(ctx, func(ctx context.Context) ([]*R, int64, error) {
return g.doQuery(ctx)
})
}
// doQuery 执行实际的 GORM 查询逻辑
func (g *GormBuilder[R]) doQuery(ctx context.Context) (list []*R, total int64, err error) {
if err = util.WaitAndGo(func() error {
query := g.builder.data.DB.WithContext(ctx).Model(new(R))
if g.filter != nil {
query = query.Scopes(g.filter) // 直接使用,无需类型断言!
}
if g.sort != nil {
query = query.Scopes(g.sort) // 直接使用,无需类型断言!
}
if g.builder.needPagination {
if g.builder.limit < 1 {
g.builder.limit = defaultLimit
}
query = query.Offset(int(g.builder.start)).Limit(int(g.builder.limit))
}
return query.Find(&list).Error
}, func() error {
if !g.builder.needTotal {
return nil
}
query := g.builder.data.DB.WithContext(ctx).Model(new(R))
if g.filter != nil {
query = query.Scopes(g.filter)
}
return query.Count(&total).Error
}); err != nil {
return nil, 0, err
}
return list, total, nil
}MongoBuilder:MongoDB 专属构建器
// MongoFilter MongoDB 过滤条件类型
type MongoFilter = bson.D
// MongoSort MongoDB 排序条件类型
type MongoSort = bson.D
// MongoBuilder MongoDB 专属查询构建器
type MongoBuilder[R any] struct {
builder[*MongoBuilder[R], R] // 自引用:B = *MongoBuilder[R]
filter MongoFilter // 强类型 bson.D
sort MongoSort // 强类型 bson.D
}
func (m *MongoBuilder[R]) self() *MongoBuilder[R] {
return m
}
func NewMongoBuilder[R any](data *DBProxy) *MongoBuilder[R] {
m := &MongoBuilder[R]{}
m.builder.data = data
m.builder.setSelf(m, m)
return m
}
// SetFilter 设置 MongoDB 过滤条件——强类型 bson.D
func (m *MongoBuilder[R]) SetFilter(filter MongoFilter) *MongoBuilder[R] {
m.filter = filter
return m
}
// SetSort 设置 MongoDB 排序条件——强类型 bson.D
func (m *MongoBuilder[R]) SetSort(sort MongoSort) *MongoBuilder[R] {
m.sort = sort
return m
}ElasticSearchBuilder:ES 专属构建器
// ElasticSearchBuilder ElasticSearch 专属查询构建器
type ElasticSearchBuilder[R any] struct {
builder[*ElasticSearchBuilder[R], R]
index string // ES 索引名,仅 ES 构建器专属
filter elastic.Query // 强类型 elastic.Query
sort []elastic.Sorter // 强类型 []elastic.Sorter
}
func (e *ElasticSearchBuilder[R]) self() *ElasticSearchBuilder[R] {
return e
}
func NewElasticSearchBuilder[R any](data *DBProxy, index string) *ElasticSearchBuilder[R] {
e := &ElasticSearchBuilder[R]{index: index}
e.builder.data = data
e.builder.setSelf(e, e)
return e
}
// SetESIndex 设置 ES 索引名——仅 ES 构建器专属方法
func (e *ElasticSearchBuilder[R]) SetESIndex(index string) *ElasticSearchBuilder[R] {
e.index = index
return e
}
// SetFilter 设置 ES 过滤条件——强类型 elastic.Query
func (e *ElasticSearchBuilder[R]) SetFilter(filter elastic.Query) *ElasticSearchBuilder[R] {
e.filter = filter
return e
}
// SetSort 设置 ES 排序条件——强类型 []elastic.Sorter
func (e *ElasticSearchBuilder[R]) SetSort(sort ...elastic.Sorter) *ElasticSearchBuilder[R] {
e.sort = sort
return e
}Scope 辅助函数:一行代码设置 filter/sort
为了在 List 模式下更便捷地设置 filter/sort,新版引入了 ScopeConfigurer 机制:
// ScopeConfigurer 构建器配置回调类型
type ScopeConfigurer[R any] func(querier Querier[R])
// NewGormScope 创建 GORM 构建器的 ScopeConfigurer
func NewGormScope[R any](filter func(*gorm.DB) *gorm.DB, sort func(*gorm.DB) *gorm.DB) ScopeConfigurer[R] {
return func(querier Querier[R]) {
if gb, ok := querier.(*GormBuilder[R]); ok {
if filter != nil {
gb.SetFilter(filter)
}
if sort != nil {
gb.SetSort(sort)
}
}
}
}
// NewMongoScope 创建 MongoDB 构建器的 ScopeConfigurer
func NewMongoScope[R any](filter bson.D, sort bson.D) ScopeConfigurer[R] {
return func(querier Querier[R]) {
if mb, ok := querier.(*MongoBuilder[R]); ok {
if filter != nil {
mb.SetFilter(filter)
}
if sort != nil {
mb.SetSort(sort)
}
}
}
}
// NewElasticSearchScope 创建 ES 构建器的 ScopeConfigurer
func NewElasticSearchScope[R any](filter elastic.Query, sort ...elastic.Sorter) ScopeConfigurer[R] {
return func(querier Querier[R]) {
if eb, ok := querier.(*ElasticSearchBuilder[R]); ok {
if filter != nil {
eb.SetFilter(filter)
}
if len(sort) > 0 {
eb.SetSort(sort...)
}
}
}
}工厂函数与 List 模式:统一的查询入口
通用工厂函数
// NewBuilder 通用工厂函数,根据 DataSource 枚举值创建对应的专属查询构建器
func NewBuilder[R any](ds DataSource, data *DBProxy) Querier[R] {
switch ds {
case MySQL:
return NewGormBuilder[R](data)
case MongoDB:
return NewMongoBuilder[R](data)
case ElasticSearch:
return NewElasticSearchBuilder[R](data, "")
default:
panic(fmt.Sprintf("unsupported data source: %d", ds))
}
}新版 List 模式
// List 查询列表功能结构
// 泛型参数:
// R - 返回结果类型参数
type List[R any] struct {
dataSource DataSource
querier Querier[R] // 可选:直接注入自定义 Querier
middlewares []Middleware[R]
scope ScopeConfigurer[R] // 可选:构建器配置回调
}
func NewList[R any]() *List[R] {
return &List[R]{}
}
// Query 执行查询
func (l *List[R]) Query(
ctx context.Context,
opts ...QueryOption,
) (result []*R, total int64, err error) {
defer func() {
if r := recover(); r != nil {
result = nil
total = 0
err = fmt.Errorf("query panic recovered: %v", r)
}
}()
options := LoadQueryOptions(opts...)
var querier Querier[R]
if l.querier != nil {
querier = l.querier
} else {
querier = NewBuilder[R](l.dataSource, options.GetData())
}
querier.SetStart(options.GetStart())
querier.SetLimit(options.GetLimit())
querier.SetNeedTotal(options.GetNeedTotal())
querier.SetNeedPagination(options.GetNeedPagination())
// 应用 Scope 配置回调
if l.scope != nil {
l.scope(querier)
}
for _, m := range l.middlewares {
querier.Use(m)
}
return querier.QueryList(ctx)
}对比旧版 List,新版有三个显著变化:
- 不再需要
Service接口——filter/sort 通过ScopeConfigurer或直接在专属构建器上设置 - 不再需要
Strategy接口——每个专属构建器自带查询逻辑 - 新增
SetQuerier方法——支持注入MockQuerier进行单元测试
实战对比:新旧调用方式
旧版调用(策略模式)
// 需要实现 Service 接口
type QueryGoods struct {
builder[model.Goods]
filter *pb.ListGoodsFilter
sort pb.QueryGoodsListSort
}
// filter 返回 any,运行时才知道类型对不对
func (query *QueryGoods) getQueryListFilter(context.Context) (any, error) {
return func(db *gorm.DB) *gorm.DB {
if query.filter.GetName() != "" {
db.Where("name = ?", query.filter.GetName())
}
return db
}, nil
}
// sort 返回 any
func (query *QueryGoods) getQueryListSort() any {
return func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}
}
// 调用时需要手动传入 filter 和 sort 到 options
list, total, err := builder.NewQueryGoods(
builder.WithData[filter, sort](builder.NewDBProxy(r.data.db, nil, nil)),
builder.WithFilter[filter, sort](req.GetFilter()),
builder.WithSort[filter, sort](req.GetSort()),
builder.WithStart[filter, sort](req.GetStart()),
builder.WithLimit[filter, sort](req.GetLimit()),
).QueryList(ctx)新版调用方式一:直接使用专属构建器(推荐)
// 创建 GORM 构建器,所有类型在编译期确定
b := builder.NewGormBuilder[model.Goods](builder.NewDBProxy(db, nil, nil))
// SetFilter 参数是 GormScope,编译器直接检查类型
b.SetFilter(func(db *gorm.DB) *gorm.DB {
return db.Where("name = ?", req.Filter.GetName())
}).SetSort(func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC")
})
b.SetStart(req.GetStart())
b.SetLimit(req.GetLimit())
b.SetNeedTotal(true)
b.SetNeedPagination(true)
users, total, err := b.QueryList(ctx)新版调用方式二:使用 List + Scope 模式
list := builder.NewList[model.Goods]()
list.SetDataSource(builder.MySQL)
// 使用 NewGormScope 一行设置 filter 和 sort
list.SetScope(builder.NewGormScope[model.Goods](
func(db *gorm.DB) *gorm.DB {
return db.Where("name = ?", req.Filter.GetName())
},
func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC")
},
))
result, total, err := list.Query(
ctx,
builder.WithData(builder.NewDBProxy(db, nil, nil)),
builder.WithStart(req.GetStart()),
builder.WithLimit(req.GetLimit()),
)架构对比:一图胜千言
旧版架构(策略模式)
┌──────────────────────────────────────────┐
│ builder[R] │ ← 单一构建器
│ filter: func(ctx) (any, error) │ ← any 类型
│ sort: func() any │ ← any 类型
│ strategy: Strategy[R] │ ← 策略接口
└──────────┬───────────────────────────────┘
│
┌──────▼──────┐ ┌──────────────┐ ┌────────────────────┐
│ GormList │ │ MongoList │ │ ElasticsearchList │
│ Strategy │ │ Strategy │ │ Strategy │
│ (类型断言) │ │ (类型断言) │ │ (类型断言) │
└─────────────┘ └──────────────┘ └────────────────────┘新版架构(自引用泛型 + 专属构建器)
┌─────────────────────────────────────────────────┐
│ Querier[R] │ ← 统一接口
│ Use / SetStart / SetLimit / SetNeedTotal / │
│ SetNeedPagination / QueryList │
└──────────┬──────────┬──────────┬────────────────┘
│ │ │
┌──────▼──┐ ┌─────▼────┐ ┌───▼─────────────┐
│ Gorm │ │ Mongo │ │ ElasticSearch │ ← 专属构建器
│ Builder │ │ Builder │ │ Builder │
│ 强类型 │ │ 强类型 │ │ 强类型 │
└──────┬──┘ └─────┬────┘ └──┬──────────────┘
│ │ │
┌──────▼──────────▼─────────▼────────────────┐
│ builder[B, R] │ ← 公共基类(自引用泛型)
│ data / start / limit / middlewares / ... │
└────────────────────────────────────────────┘选项模式的精简
新版架构中,Option 也随之精简。由于 filter/sort 不再通过 any 类型传递,而是由各专属构建器自行管理,WithFilter 和 WithSort 选项被移除:
// 旧版 Option(已移除)
// WithFilter[F, S](filter *F) ← 已删除
// WithSort[F, S](sort S) ← 已删除
// 新版 Option:只保留通用配置,不再需要泛型参数
WithData(data *DBProxy)
WithStart(start uint32)
WithLimit(limit uint32)
WithNeedTotal(needTotal bool)
WithNeedPagination(needPagination bool)
WithFields(fields ...string)这是一个重要的设计决策:filter/sort 是数据源相关的,不应该出现在通用选项中。
总结
通过 Go 1.26 自引用泛型约束特性,查询构建器实现了一次质的架构升级:
| 维度 | 旧版(策略模式) | 新版(自引用泛型) |
|---|---|---|
| 类型安全 | filter/sort 为 any,运行时断言 | 强类型,编译期检查 |
| 扩展方式 | 新增 Strategy 实现 | 新增专属 Builder |
| 链式调用 | 返回 *builder[R],丢失子类型信息 | 返回具体子类型,完整链式调用 |
| 代码量 | 策略中大量类型断言代码 | 零类型断言 |
| 中间件 | 接收 *builder[R] | 接收 Querier[R] 接口 |
| 可测试性 | 需要 mock Strategy | 内置 MockQuerier |
核心收益:
- 编译期类型安全——传错类型?编译器直接报错,不用等到运行时
- 零类型断言——告别
scope.(func(*gorm.DB) *gorm.DB)这样的惊险操作 - 真正的开闭原则——新增数据源只需创建新的专属构建器,无需修改任何已有代码
- 更好的 IDE 支持——强类型意味着自动补全、跳转定义等功能完美工作
Go 1.26 的自引用泛型约束看似只是一个小小的语法放宽,但它为 Go 语言的设计模式打开了一扇新的大门。正如这个查询构建器的演进所展示的——好的语言特性,能让好的设计自然涌现。