
本文介绍如何利用 go 的接口机制,为 sql.row 和 sql.rows 统一抽象出 scan 方法,从而复用结构体扫描逻辑,避免重复代码,体现 go “组合优于继承”的设计哲学。
本文介绍如何利用 go 的接口机制,为 sql.row 和 sql.rows 统一抽象出 scan 方法,从而复用结构体扫描逻辑,避免重复代码,体现 go “组合优于继承”的设计哲学。
在 Go 开发中,尤其是使用 database/sql 包操作数据库时,我们常需从 *sql.Row(单行查询)或 *sql.Rows(多行查询)中解包数据到结构体。但二者类型不同,无法直接共用同一扫描函数——除非借助 Go 强大的接口机制实现“鸭子类型”。
核心思路是:提取公共行为(Scan 方法),而非依赖具体类型。*sql.Row 和 *sql.Rows 均实现了签名一致的 Scan(dest ...interface{}) error 方法,因此可定义一个轻量接口来统一它们:
type rowScanner interface {
Scan(dest ...interface{}) error
}
该接口仅声明所需能力,不绑定任何具体实现,完全符合 Go 的“小接口”原则。随后,我们将原本针对 *sql.Row 编写的 ScanUserFromRow 函数升级为接受 rowScanner 接口:
func ScanUserFromRow(rs rowScanner) User {
var user User
err := rs.Scan(&user.ID, &user.FirstName, &user.LastName)
if err != nil {
panic(err) // 生产环境建议返回 error 而非 panic
}
return user
}
现在,该函数可无缝用于两种场景:
-
✅ 单行查询(*sql.Row):
func GetUserById(id int) User { sql := `SELECT id, first_name, last_name FROM users WHERE id = $1` row := db.QueryRow(sql, id) return ScanUserFromRow(row) // *sql.Row 满足 rowScanner } -
✅ 多行查询(*sql.Rows):
func GetUsers() []User { sql := `SELECT id, first_name, last_name FROM users ORDER BY last_name` rows, err := db.Query(sql) if err != nil { panic(err) } defer rows.Close() // 注意:务必 defer Close() var users []User for rows.Next() { user := ScanUserFromRow(rows) // *sql.Rows 同样满足 rowScanner users = append(users, user) } return users }
⚠️ 重要注意事项:
- Scan 方法在 *sql.Rows 上调用时,会按当前游标位置读取一行;务必确保 rows.Next() 已成功返回 true,否则 Scan 将失败。
- 接口参数支持内联定义(更简洁,适合一次性使用):
func ScanUserFromRow(rs interface{ Scan(...interface{}) error }) User { /* ... */ } - panic 仅适用于开发调试;生产代码应返回 error 并由调用方处理,例如:
func ScanUserFromRow(rs rowScanner) (User, error) { var u User if err := rs.Scan(&u.ID, &u.FirstName, &u.LastName); err != nil { return u, err } return u, nil }
这种基于行为抽象的设计,正是 Go 的惯用风格:不预设类继承体系,而是通过接口聚焦“能做什么”,让不同类型自然适配同一函数。它既保持了类型安全,又极大提升了代码复用性与可维护性。











