DAO设计应先定义Repository接口而非具体实现,如UserRepository包含FindByID等方法,不暴露SQL细节;实现需接收已初始化的sql.DB并验证健康性,事务通过函数参数传入sql.Tx,推荐sqlx减少样板代码但需注意字段映射与NULL处理,单元测试优先使用sqlmock。

DAO 接口设计要先定义 Repository 而非具体实现
Go 没有抽象类,但接口是解耦 DAO 的核心。直接写 MySQLUserDAO 或 PostgresUserDAO 会导致业务层强依赖数据库驱动细节。正确做法是先定义面向领域操作的接口,比如:
type UserRepository interface {
FindByID(id int) (*User, error)
Create(u *User) (int, error)
Update(u *User) error
Delete(id int) error
}
这个接口不暴露 SQL、driver 或连接池,只描述“能做什么”。后续可自由替换为内存实现(测试用)、Redis 实现(缓存兜底)或不同 SQL 方言实现。
SQL 驱动实现里必须显式管理 *sql.DB 生命周期
常见错误是把 *sql.DB 当作局部变量反复 sql.Open,导致连接泄漏或连接数爆炸。DAO 实现应接收已初始化好的 *sql.DB 实例,并在构造时验证其健康性:
- 使用
db.Ping()检查连接是否可用,失败时返回明确错误而非静默 panic - 避免在 DAO 方法内调用
db.Close()—— 它属于上层容器(如 HTTP server shutdown 阶段) - 若需事务支持,不要在 DAO 接口暴露
BeginTx;改用函数式参数传入*sql.Tx,例如:CreateWithTx(tx *sql.Tx, u *User) (int, error)
用 sqlx 替代原生 database/sql 可减少样板代码但要注意扫描逻辑
sqlx 提供结构体自动映射(Get/Select),但容易忽略两个关键点:
立即学习“go语言免费学习笔记(深入)”;
- 字段名匹配默认按 Go struct tag
db:"column_name",未声明时 fallback 到大驼峰转下划线(CreatedAt→created_at),但若数据库列名含大小写混合(如created_At)会失配 -
sqlx.StructScan对NULL值敏感:目标字段类型为string时,数据库NULL会报sql: Scan error on column index X: unsupported Scan, storing driver.Value type,必须改用sql.NullString等可空类型
单元测试 DAO 时优先用 sqlmock 而非真实数据库
真实 DB 测试慢、难隔离、易污染。用 sqlmock 可精准控制返回值和错误场景:
db, mock, _ := sqlmock.New()
defer db.Close()
mock.ExpectQuery(`SELECT.*FROM users`).WithArgs(123).WillReturnRows(
sqlmock.NewRows([]string{"id", "name"}).AddRow(123, "alice"),
)
repo := NewSQLUserRepository(db)
user, _ := repo.FindByID(123) // 断言 user.Name == "alice"
注意:必须调用 mock.ExpectationsWereMet() 结尾,否则未匹配的 SQL 调用会被静默忽略,导致测试假阳性。
真正麻烦的是跨 DAO 协作逻辑(比如 User + Order + Payment 联动更新),这时候接口抽象和 mock 边界就变得特别关键——漏掉一个依赖的 ExpectQuery,测试就失去意义。










