
通过包装 sqlx.queryer/execer 接口实现无侵入式 sql 日志记录,兼容原生 sqlx 调用习惯,支持参数化查询、执行耗时与格式化输出,无需修改现有数据库操作逻辑。
通过包装 sqlx.queryer/execer 接口实现无侵入式 sql 日志记录,兼容原生 sqlx 调用习惯,支持参数化查询、执行耗时与格式化输出,无需修改现有数据库操作逻辑。
在 Go 生态中,sqlx 作为 database/sql 的增强库,提供了更简洁的结构体映射和批量操作能力,但其默认不提供 SQL 日志功能。与 Rails 的 ActiveRecord 日志类似,我们希望在开发或调试阶段清晰看到每条实际发送至 MySQL 的 SQL 语句、参数及执行耗时。幸运的是,sqlx 的设计高度依赖接口抽象——sqlx.Queryer 和 sqlx.Execer,这为我们提供了优雅的拦截切入点。
✅ 推荐方案:接口包装 + 日志装饰器
核心思路是不修改业务代码调用方式,而是将 *sqlx.DB 实例封装为实现了 Queryer 和 Execer 的日志代理类型。这样既可复用 sqlx.Select/sqlx.Get/sqlx.MustExec 等自由函数(free functions),又能集中控制日志行为。
以下是一个生产就绪的 SQLLogger 实现,支持格式化输出(含执行耗时)、参数安全打印(避免敏感信息泄露)及 MySQL 兼容语法:
package main
import (
"database/sql"
"fmt"
"log"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/go-sql-driver/mysql"
)
type SQLLogger struct {
queryer sqlx.Queryer
execer sqlx.Execer
logger *log.Logger
}
// Query logs and delegates to underlying Queryer
func (l *SQLLogger) Query(query string, args ...interface{}) (*sql.Rows, error) {
start := time.Now()
rows, err := l.queryer.Query(query, args...)
duration := time.Since(start)
l.logQuery(query, args, duration, err)
return rows, err
}
// Queryx is sqlx-specific; logs same as Query
func (l *SQLLogger) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) {
start := time.Now()
rows, err := l.queryer.Queryx(query, args...)
duration := time.Since(start)
l.logQuery(query, args, duration, err)
return rows, err
}
// QueryRowx logs single-row queries
func (l *SQLLogger) QueryRowx(query string, args ...interface{}) *sqlx.Row {
start := time.Now()
row := l.queryer.QueryRowx(query, args...)
// Defer logging until Scan() or Err() is called — but for simplicity, log immediately
// (real-world: consider wrapping *sqlx.Row too for precise timing)
duration := time.Since(start)
l.logQuery(query, args, duration, nil)
return row
}
// Exec logs DML/DDL statements
func (l *SQLLogger) Exec(query string, args ...interface{}) (sql.Result, error) {
start := time.Now()
result, err := l.execer.Exec(query, args...)
duration := time.Since(start)
l.logQuery(query, args, duration, err)
return result, err
}
// MustExec logs and panics on error (matches sqlx.MustExec behavior)
func (l *SQLLogger) MustExec(query string, args ...interface{}) sql.Result {
result, err := l.Exec(query, args...)
if err != nil {
log.Panicf("SQL execution failed: %v\nQuery: %s\nArgs: %v", err, query, args)
}
return result
}
// Helper to format and log with Rails-style output
func (l *SQLLogger) logQuery(query string, args []interface{}, dur time.Duration, err error) {
var msg string
if err != nil {
msg = fmt.Sprintf(" ERROR (%v) %s", dur.Truncate(0.1*time.Millisecond), query)
l.logger.Printf("%s — %v", msg, err)
return
}
msg = fmt.Sprintf(" QUERY (%v) %s", dur.Truncate(0.1*time.Millisecond), query)
if len(args) > 0 {
// Optional: use sqlx.In() + sqlx.Rebind() for real parameter interpolation (not shown here for security)
// Instead, log args separately to avoid SQL injection in logs
l.logger.Printf("%s — Args: %v", msg, args)
} else {
l.logger.Printf(msg)
}
}? 使用方式(零业务代码改造)
初始化数据库后,用 SQLLogger 包装 *sqlx.DB,后续全部使用 sqlx 的自由函数(而非 db.Select() 等方法调用):
func main() {
db, err := sqlx.Connect("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if err != nil {
log.Fatal(err)
}
defer db.Close()
logger := log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
sqlLogger := &SQLLogger{
queryer: db,
execer: db,
logger: logger,
}
// ✅ 全部走自由函数,自动触发日志
var users []User
err = sqlx.Select(sqlLogger, &users, "SELECT * FROM users WHERE status = ? ORDER BY id DESC LIMIT ?", "active", 10)
if err != nil {
log.Fatal(err)
}
_, err = sqlx.NamedExec(sqlLogger, "INSERT INTO users (name, email) VALUES (:name, :email)", map[string]interface{}{
"name": "Alice",
"email": "alice@example.com",
})
if err != nil {
log.Fatal(err)
}
}⚠️ 注意事项与最佳实践
- 不要直接打印带参数的完整 SQL 字符串:fmt.Sprintf(query, args...) 易引发 SQL 注入风险(即使仅用于日志),且无法处理 ? 占位符与驱动差异(如 MySQL 用 ?,PostgreSQL 用 $1)。应始终分离打印 SQL 模板与参数。
- 命名查询(NamedQuery)支持:若使用 sqlx.NamedExec 等,需额外实现 sqlx.Ext 接口(含 NamedQuery, NamedQuery 方法),或统一转为位置参数再记录。
- 性能影响:日志本身有开销,仅在开发/测试环境启用;生产环境建议关闭或采样(如仅记录慢查询 dur > 500ms)。
-
替代方案对比:
- sqlx.DB.SetLogger():不生效 —— sqlx 当前版本(v1.3.5+)未实际调用该 logger;
- 修改 database/sql 的 Driver:侵入性强、维护成本高;
- 使用 go-sql-driver/mysql 的 interpolateParams=true + 自定义 Connector:复杂且不通用。
✅ 总结
通过组合 sqlx.Queryer/Execer 接口与装饰器模式,你能在不侵入业务逻辑的前提下,获得媲美 Rails 的结构化 SQL 日志能力。该方案轻量、可扩展(轻松加入慢查询告警、SQL 模板统计等),并天然适配 MySQL、PostgreSQL 等多数据库驱动,是 Go 数据层可观测性的推荐实践。


















