因为database/sql是无状态查询接口,不提供变更通知机制,仅返回影响行数,无法自动触发下游逻辑;硬编码回调会导致业务与数据访问耦合、事务边界模糊、难以复用,而postgresql listen/notify、mysql binlog、sqlite updatehook才是正解。

为什么不能直接用 database/sql 的 Query 监听变更
因为 database/sql 是无状态的查询接口,它不提供任何数据变更通知机制。执行 INSERT、UPDATE 或 DELETE 后,你只能拿到影响行数(RowsAffected),但无法自动触发下游逻辑——除非你自己在每次写操作后手动调用回调函数。
真实场景中,这种“写完立刻通知”的硬编码方式会导致:业务逻辑和数据访问耦合严重、事务边界模糊(比如回调里再写 DB 就可能破坏 ACID)、难以复用到多个表或服务。
- 别试图给
sql.Tx打补丁来“监听”,它不是设计来干这事的 - PostgreSQL 的
LISTEN/NOTIFY、MySQL 的 binlog、SQLite 的sqlite3_update_hook才是正路 - 框架层(如 Gin、Echo)本身也不处理数据变更事件,那是 ORM 或数据访问层的事
用 pglogrepl 实现 PostgreSQL 变更捕获(推荐方案)
如果你用的是 PostgreSQL,并且需要实时、可靠、低延迟的数据变更监听,pglogrepl 是目前 Go 生态最轻量又可控的选择。它基于逻辑复制协议,能精确捕获 INSERT/UPDATE/DELETE 事件,包含新旧行数据、事务时间戳和 LSN。
注意:必须开启 PostgreSQL 的逻辑复制支持(wal_level = logical,max_replication_slots ≥1,max_wal_senders ≥2)。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 不要用
pglogrepl连接普通用户账号——需创建专用复制角色:CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '...'; - 订阅前必须先创建复制槽(
CreateReplicationSlot),否则启动后会丢失历史变更 - 每条
pglogrepl.ChangeEvent包含Relation(表名)、OldTuple和NewTuple,字段顺序与表定义一致,不是按列名映射的 map - 解析时别直接用
json.Marshal原始ChangeEvent——它含二进制字段,应先调用ChangeEvent.Unmarshal提取结构化数据
用 go-sqlite3 的 UpdateHook 监听本地 SQLite 变更
SQLite 虽然是嵌入式数据库,但它提供了成熟的 C API 钩子机制。Go 的 go-sqlite3 绑定暴露了 UpdateHook,可在每次行级变更时触发回调,适合单机应用、测试环境或边缘设备。
关键限制:只在当前连接生效,不跨进程;不支持事务内部分变更回滚后的撤销通知;钩子函数必须是同步执行,阻塞写操作。
- 注册钩子必须在
sql.Open后、首次Exec前完成:db.Driver().(sqlite3.Driver).SetUpdateHook(...) - 回调参数中的
op是整数,对应SQLITE_INSERT(1)、SQLITE_UPDATE(2)、SQLITE_DELETE(3) - 获取字段值要用
sqlite3.ColumnText等 C 函数,Go 层没封装成 struct——你需要自己根据table名查 schema,再逐列读取 - 别在钩子里做网络请求或长耗时操作,否则拖慢所有写请求
避免在 ORM 层(如 GORM)里“模拟监听”
GORM 的 Callback(如 BeforeUpdate)看起来像监听器,但它只是拦截 ORM 自己生成的 SQL,对原生 db.Exec("UPDATE ...")、存储过程、其他客户端写入完全无效。而且它运行在事务内部,若回调 panic 会导致整个事务 rollback,行为不可控。
- 不要依赖
GORM的Save回调做审计日志——漏掉 raw SQL 就等于丢数据 -
AfterFind是读操作钩子,和变更监听无关,别混用 - 如果非要用 GORM,唯一安全做法是:所有写操作统一走 GORM,且禁用
db.Exec;同时把变更事件发到消息队列(如github.com/segmentio/kafka-go),由独立消费者处理 - GORM v2 的
Session支持WithContext传入自定义上下文,可用于透传 trace ID 或租户信息,但不能替代真正的变更捕获
真正难的不是怎么监听,而是怎么确保事件不丢、不错序、可重放。LSN、binlog position、WAL offset 这些才是锚点,不是加个回调函数就叫事件驱动。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










