
本文介绍在基于 sqlx + MySQL 的 Go 项目中,无需框架即可实现 SQL 语句的统一日志输出,通过封装 Queryer/Execer 接口实现轻量级、非侵入式 SQL 日志记录,并支持格式化耗时与参数打印。
本文介绍在基于 sqlx + mysql 的 go 项目中,无需框架即可实现 sql 语句的统一日志输出,通过封装 `queryer`/`execer` 接口实现轻量级、非侵入式 sql 日志记录,并支持格式化耗时与参数打印。
在 Go 生态中,sqlx 并未内置 SQL 日志功能,但其精巧的接口设计(如 sqlx.Queryer 和 sqlx.Execer)为日志注入提供了天然支持。核心思路是:*不直接使用 `sqlx.DB的链式方法(如db.Select()),而是将其作为接口实现体,包裹进自定义日志代理类型,再交由sqlx的自由函数(如sqlx.Select()、sqlx.Get()`)调用**——这样既保留全部功能,又可集中拦截 SQL 执行行为。
以下是一个生产就绪的 QueryLogger 实现,支持标准查询、带命名参数的查询及单行查询,并自动记录执行耗时(模拟 Rails 风格输出):
import (
"database/sql"
"fmt"
"log"
"time"
"github.com/jmoiron/sqlx"
)
type QueryLogger struct {
queryer sqlx.Queryer
execer sqlx.Execer
logger *log.Logger
}
// Query 记录普通查询(如 SELECT)
func (ql *QueryLogger) Query(query string, args ...interface{}) (*sql.Rows, error) {
start := time.Now()
rows, err := ql.queryer.Query(query, args...)
elapsed := time.Since(start)
ql.logger.Printf("%s (%v) %s",
extractQueryType(query),
formatDuration(elapsed),
formatQueryWithArgs(query, args...))
return rows, err
}
// Queryx 支持 sqlx.Rows(含 StructScan 等扩展能力)
func (ql *QueryLogger) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) {
start := time.Now()
rows, err := ql.queryer.Queryx(query, args...)
elapsed := time.Since(start)
ql.logger.Printf("%s (%v) %s",
extractQueryType(query),
formatDuration(elapsed),
formatQueryWithArgs(query, args...))
return rows, err
}
// QueryRowx 记录单行查询(如 SELECT ... LIMIT 1)
func (ql *QueryLogger) QueryRowx(query string, args ...interface{}) *sqlx.Row {
start := time.Now()
row := ql.queryer.QueryRowx(query, args...)
// 注意:QueryRowx 不返回 error,需在 Scan 时检查
elapsed := time.Since(start)
ql.logger.Printf("%s (%v) %s",
extractQueryType(query),
formatDuration(elapsed),
formatQueryWithArgs(query, args...))
return row
}
// Exec 记录写操作(INSERT/UPDATE/DELETE)
func (ql *QueryLogger) Exec(query string, args ...interface{}) (sql.Result, error) {
start := time.Now()
result, err := ql.execer.Exec(query, args...)
elapsed := time.Since(start)
ql.logger.Printf("%s (%v) %s",
extractQueryType(query),
formatDuration(elapsed),
formatQueryWithArgs(query, args...))
return result, err
}
// 辅助函数:提取 SQL 类型(如 "User Load")
func extractQueryType(query string) string {
query = strings.TrimSpace(strings.ToUpper(query))
switch {
case strings.HasPrefix(query, "SELECT"):
if fromIdx := strings.Index(query, "FROM"); fromIdx > 0 {
table := strings.Fields(query[fromIdx+4:])[0]
return fmt.Sprintf("%s Load", strings.Trim(table, "`"))
}
return "SELECT"
case strings.HasPrefix(query, "INSERT"):
return "INSERT"
case strings.HasPrefix(query, "UPDATE"):
return "UPDATE"
case strings.HasPrefix(query, "DELETE"):
return "DELETE"
default:
return "SQL"
}
}
// 辅助函数:格式化耗时(如 "94.4ms")
func formatDuration(d time.Duration) string {
if d <p>使用方式如下(初始化阶段):</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件"><img
src="https://img.php.cn/upload/webcode/000/000/164/636a2b4d84031727.png" alt="使用Go语言搭建家庭相册系统-相关课件" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</a>
<p class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</p>
</div>
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><pre class="brush:php;toolbar:false;">db, err := sqlx.Connect("mysql", "user:pass@tcp(127.0.0.1:3306)/mydb")
if err != nil {
log.Fatal(err)
}
// 创建带日志的包装器
ql := &QueryLogger{
queryer: db,
execer: db,
logger: log.New(os.Stdout, "", log.LstdFlags),
}
// ✅ 正确:使用 sqlx 自由函数 + 包装器
var users []User
err = sqlx.Select(ql, &users, "SELECT * FROM users WHERE login = ? LIMIT 1", "bondnewyork")
// ✅ 同样适用于 Get、GetStruct、MustExec 等
var user User
err = sqlx.Get(ql, &user, "SELECT * FROM users WHERE id = ?", 123)⚠️ 重要注意事项:
- 此方案要求*统一使用 `sqlx.自由函数**(而非db.Select()`),否则日志不会生效;
- 若已有大量 db.Xxx() 调用,可考虑用 sqlx.Unsafe + 自定义 DB 子类,或改用 sqlmock(测试场景)或 go-sql-log(生产增强版);
- 切勿在日志中直接打印原始参数值(如密码、token),应启用字段白名单或使用 log.Printf("... WHERE id = ?") 占位符模式;
- 如需精确耗时(含网络延迟),建议结合 context.Context 和中间件式拦截,或使用 database/sql 的 driver.DriverContext(Go 1.19+)。
通过该方式,你将获得清晰、结构化的 SQL 日志输出,大幅提升调试效率,同时保持代码简洁与数据库驱动无关性。










