ARTICLE · 1002842
Gorm链式调用API源码解析 - Distinct
Distinct 方法解析
一、方法签名与原型
// Distinct specify distinct fields that you want querying//// // Select distinct names of users// db.Distinct("name").Find(&results)// // Select distinct name/age pairs from users// db.Distinct("name", "age").Find(&results)func(db *DB) Distinct(args ...interface{}) (tx *DB)参数说明:
• db *DB:GORM DB 指针接收者• args ...interface{}:可选的可变参数,表示需要 DISTINCT 的字段名列表。如果传入参数,会被转发给Select方法同时设置 SELECT 字段;如果不传参数,则仅将当前已有的 Select 字段标记为 DISTINCT
返回值:
• tx *DB:新的 DB 实例副本,用于链式调用
二、完整源码
func(db *DB) Distinct(args ...interface{}) (tx *DB) { tx = db.getInstance() tx.Statement.Distinct = trueiflen(args) > 0 { tx = tx.Select(args[0], args[1:]...) }return}三、逐行原理讲解
第 1 行:方法声明
func(db *DB) Distinct(args ...interface{}) (tx *DB) {• 与多数链式 API 类似,返回 *DB用于链式调用• 参数 args ...interface{}为可变参数,使用interface{}而非string是为了兼容Select方法多样化的入参形式(字符串、切片、map 等)• 即使不传任何参数( db.Distinct())也是合法调用,语义为:对后续 SELECT 的字段启用去重
第 2 行:获取副本实例
tx = db.getInstance()标准的副本克隆操作。无论 Distinct 后续逻辑如何,都保证不污染原始 DB 对象。
第 3 行:设置 Distinct 标志位
tx.Statement.Distinct = true这是 Distinct 方法的核心作用。它将 Statement 上的布尔字段 Distinct 设为 true。
这个标志位在后续 SQL 编译阶段会被读取:
• 在 clause.Select结构体的Build方法中,如果Distinct == true,会在字段列表前插入关键字DISTINCT• 即编译时会生成: SELECT DISTINCT field1, field2 FROM ...而非默认的SELECT field1, field2 FROM ...
值得注意的是,Distinct 本身不直接操纵 Clauses map,而是通过 Statement 级别的字段影响 SELECT clause 的构建。这是一种"标志位驱动 Clause"的间接模式。
第 4-6 行:如果传入了字段参数,则转发调用 Select
iflen(args) > 0 { tx = tx.Select(args[0], args[1:]...) }这是一个非常精妙的便捷性设计:
• 如果用户没有在 Distinct 中传字段,则需要显式搭配 .Select(...)使用• 如果用户传了字段,则相当于自动为其调用了 Select,省去了一次方法链调用• 注意转发的参数形式是 args[0]单独传,args[1:]作为后续可变参数展开。这是因为Select方法的签名是Select(query interface{}, args ...interface{}):第一个参数是主参数(可以是 string、[]string 等),后续是可选参数
由于 Select 本身会返回一个新的 tx 副本,所以这里要将返回值重新赋值给 tx,确保链式调用的状态能正确叠加。
第 7 行:返回 tx
return裸返回 tx,返回的副本既包含了 Distinct = true 的标志,又(如果有参数的话)包含了新设置的 SELECT 字段。
四、核心设计思路
4.1 组合优于扩展:复用 Select 逻辑
Distinct 方法只有 7 行代码,其中的关键设计决策是:不重复实现字段解析逻辑,而是复用已有的 Select 方法。
如果 Distinct 自己实现一套字段解析逻辑(区分 []string、string、带占位符 SQL 等),不仅会和 Select 形成大量代码重复,还会导致未来 Select 逻辑更新时 Distinct 必须同步维护的一致性问题。
通过转发给 Select 方法:
• 代码量最小化(DRY 原则) • 行为一致性有保障: Distinct("name", "age")和Select("name", "age").Distinct()结果完全一致• 未来 Select 新增能力时(如命名参数 @形式),Distinct 自动获得相同能力
4.2 标志位模式的声明式设计
SQL 层面的 SELECT DISTINCT 在语法上被视为对 SELECT 子句的修饰,而非独立子句。GORM 在建模时也忠实反映了这一点:
• 没有单独的 Clause.Distinct子句• 而是在 Statement上设置一个布尔标志位Distinct• 最终由 clause.Select.Build()方法在编译时读取该标志并生成DISTINCT关键字
这种"标志位 + 关联 Clause 编译时消费"的模式,比设计独立子句更贴近 SQL 真实语法结构,也减少了 Clause 合并时的复杂度。
4.3 两种使用模式的统一
Distinct 的设计允许用户在两种心智模型中自由切换:
模式 A:把 DISTINCT 当作修饰符
db.Select("name", "age").Distinct().Find(&results)模式 B:把 DISTINCT 当作 Select 的变体
db.Distinct("name", "age").Find(&results)两种模式在 Statement 状态上完全等价。用户可以按自己的习惯选择写法,API 同时支持两者而不产生额外成本。
4.4 与 Select 的前向兼容
第 4 行的 if len(args) > 0 判断是一个前向兼容设计。如果用户调用 db.Distinct() 不带参数,意味着他们想先设置标志位,稍后再通过 Select 指定字段。这在动态查询构造中是常见需求:
query := db.Model(&User{}).Distinct()if needOnlyName { query = query.Select("name")} else { query = query.Select("*")}query.Find(&results)4.5 参数转发的边界处理
注意转发形式是 tx.Select(args[0], args[1:]...) 而非 tx.Select(args...)。这对应 Select 方法签名的不对称性:第一个参数叫 query(主类型),后续参数叫 args(绑定变量或额外字段)。这种转发确保了:
• Distinct("name", "age")→Select("name", "age")— 两个字段字符串,正确• Distinct([]string{"name", "age"})→Select([]string{"name", "age"})— 切片形式,正确• Distinct("name = ? OR name = ?", "a", "b")→ 即使传入占位符形式,Select 也能正确处理
五、使用示例
示例 1:单列去重查询
// Go 代码var names []stringdb.Model(&User{}).Distinct("name").Find(&names)生成的 SQL:
SELECTDISTINCT name FROM users说明:
• Distinct 同时完成了"设置标志位"和"调用 Select('name')"两件事 • 结果:返回所有不重复的用户姓名
示例 2:多列组合去重
// Go 代码type NameAge struct { Name string Age int}var results []NameAgedb.Table("users").Distinct("name", "age").Scan(&results)生成的 SQL:
SELECTDISTINCT name, age FROM users说明:
• 传入 "name" 和 "age" 两个参数,转发为 Select("name", "age") • 多列去重时 SQL 会按 (name, age) 组合进行去重 • 使用 Scan 接收自定义结构体结果
示例 3:与 Where 条件、Order 组合使用(先标记 DISTINCT 后 Select)
// Go 代码var users []Userdb.Model(&User{}). Distinct(). Where("age > ?", 18). Select("city"). Order("city"). Find(&users)生成的 SQL:
SELECTDISTINCT city FROM users WHERE age >18ORDERBY city说明:
• 这里使用了"先标记 DISTINCT + 后 Select"的拆分模式 • 适合中间需要动态插入条件的场景 • WHERE 和 ORDER BY 不会影响 DISTINCT 修饰作用,最终 SQL 正确生成 DISTINCT
与Ai配合,对gorm的链式api中的Distinct方法的源码做了一个解读,记录到公众号,方便后续查阅。